#はじめに
Slateでインポートオプションのダイアログ作れた!
— Naotsun (@Naotsun_UE) January 21, 2020
IDetailsView使うと入力部分は全部勝手にやってくれるから便利だった!#UE4 #UE4Study pic.twitter.com/yiE0CmqfOF
PDFをアセットとして読み込むことができるプラグインで、PDFをインポートする際にページの範囲指定や解像度を指定できるように上のツイートのようなインポートオプションを作成したので、その作り方についてご紹介したいと思います。
自作アセットの作り方についてはヒストリアさんが、詳しく解説されていますのでまずは以下の記事を読むことをお勧めします。
[UE4] 独自のアセットを実装する方法(1) アセットクラスの実装
[UE4] 独自のアセットを実装する方法(2) インポートの実装
[UE4] 独自のアセットを実装する方法(3) 再インポートの実装
[UE4] 独自のアセットを実装する方法(4) アセットにアクションを追加する
[UE4] 独自のアセットを実装する方法(5) アセットエディタを実装する
[UE4] 独自のアセットを実装する方法(6) 独自のシリアライズを実装する
#つくるもの
今回はPDFアセットの例を紹介するので上の画像のようなインポートオプションを作成します。
スタティックメッシュアセットなどと同様にインポート時と再インポートに表示されるようにします。
#つくってみる
まずは、インポートオプションとして表示するUIのクラスをSlateを用いて作成します。
以下がヘッダーファイルと実装ファイルです。記事が長くなるのでGithubのURLになります。
PDFImportOptions.h
PDFImportOptions.cpp
まずはヘッダーファイルから見ていきましょう。
UCLASS()
class PDFIMPORTERED_API UPDFImportOptions : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PageRange")
bool SpecifyPageRange;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PageRange", meta = (ClampMin = 1, UIMin = 1, EditCondition = "SpecifyPageRange"))
int FirstPage;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PageRange", meta = (ClampMin = 1, UIMin = 1, EditCondition = "SpecifyPageRange"))
int LastPage;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dpi")
int Dpi;
public:
UPDFImportOptions() : SpecifyPageRange(false), FirstPage(1), LastPage(1), Dpi(150) {}
};
まず、インポートオプションで入力するパラメータをメンバ変数に持つUObjectを作成します。UPROPERTYを付けたメンバ変数とコンストラクタだけ定義します。
class SPDFImportOptions : public SCompoundWidget
{
private:
UPDFImportOptions* ImportOptions;
bool bShouldImport;
TWeakPtr<class SWindow> WidgetWindow;
TSharedPtr<class IDetailsView> DetailsView;
public:
SLATE_BEGIN_ARGS(SPDFImportOptions)
: _WidgetWindow()
, _ImportOptions(nullptr)
{}
SLATE_ARGUMENT(TSharedPtr<SWindow>, WidgetWindow)
SLATE_ARGUMENT(UPDFImportOptions*, ImportOptions)
SLATE_ARGUMENT(FText, Filename)
SLATE_END_ARGS()
public:
SPDFImportOptions();
void Construct(const FArguments& InArgs);
// Button reaction
FReply OnImport();
FReply OnCancel();
// End of Button reaction
// Import was done
bool ShouldImport() const { return bShouldImport; }
};
次にSCompoundWidget
を継承したクラスを作成します。UnrealC++ではSlateに関するクラスの接頭子はSになります。
まず、プライベートなメンバ変数として
・先程作成したパラメータのUObjectクラスのポインタ
・インポートされたか(キャンセルボタンを押した場合false)
・親のウィンドウのポインタ
・この記事の主役のポインタ(詳しくは後で説明します)
の4つを定義します。
次にSlateクラス特有の構文を見ていきましょう。SLATE_BEGIN_ARGS
とSLATE_END_ARGS
の二つのマクロに囲まれた部分がSlateクラスに外部から値を渡すために必要な構文です。
ここでSLATE_ARGUMENT
で変数型と変数名を登録すると下のConstruct関数の引数のInArgs
のメンバ変数となり、関数内で値を使用することができます。
また、パラメータのUObjectクラスが引数にある理由は外部で作成したインスタンスの値をこのSlateクラスで変えて、結果を返却する関数が不要になるためです。
次にコンストラクタと先程のConstruct関数、インポートボタンとキャンセルボタンが押された時に呼び出される関数、インポートされたかを取得する関数を定義します。
次は実装ファイルを見ていきましょう。
コンストラクタは特に何もしていないので飛ばします。
void SPDFImportOptions::Construct(const FArguments& InArgs)
{
ImportOptions = InArgs._ImportOptions;
WidgetWindow = InArgs._WidgetWindow;
check(ImportOptions)
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
FDetailsViewArgs DetailsViewArgs;
DetailsViewArgs.bAllowSearch = false;
DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea;
DetailsView = PropertyEditorModule.CreateDetailView(DetailsViewArgs);
DetailsView->SetObject(ImportOptions);
this->ChildSlot
[
SNew(SVerticalBox)
// ファイルパス
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(2)
[
SNew(SBorder)
.Padding(FMargin(3))
.BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder"))
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(STextBlock)
.Font(FEditorStyle::GetFontStyle("CurveEd.LabelFont"))
.Text(LOCTEXT("Import_CurrentFileTitle", "Current File: "))
]
+ SHorizontalBox::Slot()
.Padding(5, 0, 0, 0)
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Font(FEditorStyle::GetFontStyle("CurveEd.InfoFont"))
.Text(InArgs._Filename)
]
]
]
// 入力欄
+ SVerticalBox::Slot()
.Padding(2)
.MaxHeight(500.0f)
[
DetailsView->AsShared()
]
// インポートとキャンセルのボタン
+ SVerticalBox::Slot()
.AutoHeight()
.HAlign(HAlign_Right)
.Padding(2)
[
SNew(SUniformGridPanel)
.SlotPadding(2)
+ SUniformGridPanel::Slot(0, 0)
[
SNew(SButton)
.HAlign(HAlign_Center)
.Text(LOCTEXT("PDFImportOptions_Import", "Import"))
.OnClicked(this, &SPDFImportOptions::OnImport)
]
+ SUniformGridPanel::Slot(1, 0)
[
SNew(SButton)
.HAlign(HAlign_Center)
.Text(LOCTEXT("PDFImportOptions_Cancel", "Cancel"))
.ToolTipText(LOCTEXT("PDFImportOptions_Cancel_ToolTip", "Cancels importing this PDF file"))
.OnClicked(this, &SPDFImportOptions::OnCancel)
]
]
];
}
Construct関数内では主に初期化とSlateを用いたUIの構成を記述します。
最初に、先程説明したようにInArgsにメンバ変数で親ウィンドウのポインタとパラメータのUObjectクラスを初期化します。
次に先程後で説明すると言った主役、IDetailsView
クラスの登場です。このクラスは渡したUObjectクラスが持つUPROPERTY付のメンバ変数をBPの編集画面などでよく見るリスト形式にして表示してくれます。
自分でSlateを用いてリストを構築する必要がなく、パラメータの追加や削除はUObjectクラスのメンバ変数を変更するだけで良いのでとても便利なクラスです。
初期化の手順は以下の通りです。
-
FDetailsViewArgs
型の変数を作成し、パラメータを設定する。 -
PropertyEditor
モジュールをロード -
IDetailsView
型のメンバ変数をFPropertyEditorModule::CreateDetailView
関数で初期化 -
IDetailsView::SetObject
関数でパラメータのUObjectクラスを設定する
FDetailsViewArgs
のパラメータでは検索欄の表示やスクロールバーの表示など様々なことを設定することができます。
次にSlateでUIを構築する部分ですが、Slateについては話が長くなるので詳しく説明しません。
ボタンなどに表示される文字を変えたい場合は.Text(LOCTEXT("~~","~~"))
の2番目の文字列を編集してください。
ファイルパスとボタンの部分はSlateで色々とめんどうなことをやっていますが、入力欄はIDetailsView::AsShared
関数だけなのでIDetailsView
クラスがいかに便利かがわかるかと思います。
FReply SPDFImportOptions::OnImport()
{
bShouldImport = true;
if (!ImportOptions->SpecifyPageRange)
{
ImportOptions->FirstPage = 0;
ImportOptions->LastPage = 0;
}
if (WidgetWindow.IsValid())
{
WidgetWindow.Pin()->RequestDestroyWindow();
}
return FReply::Handled();
}
FReply SPDFImportOptions::OnCancel()
{
bShouldImport = false;
if (WidgetWindow.IsValid())
{
WidgetWindow.Pin()->RequestDestroyWindow();
}
return FReply::Handled();
}
次にボタンが押された時に呼び出される関数ですが、ここでは主にbShouldImport
の値を結果に応じて変えます。
WidgetWindow.Pin()->RequestDestroyWindow
関数で表示しているウィンドウを閉じることができます。
少し長くなりましたがこれでインポートオプションを入力するUIのクラスが完成しました。
次にこのUIを表示する部分を見ていきましょう。この部分はインポートや再インポートに関するUFactory
を継承したクラスに記述します。
以下がヘッダーファイルと実装ファイルです。こちらも記事が長くなるのでGithubのURLになります。
PDFFactory.h
PDFFactory.cpp
private:
// Display a dialog to enter import options
void ShowImportOptionWindow(TSharedPtr<class SPDFImportOptions>& Options, const FString& Filename, class UPDFImportOptions* &Result);
void UPDFFactory::ShowImportOptionWindow(TSharedPtr<SPDFImportOptions>& Options, const FString& Filename, UPDFImportOptions* &Result)
{
TSharedRef<SWindow> Window = SNew(SWindow)
.Title(LOCTEXT("WindowTitle", "PDF Import Options"))
.SizingRule(ESizingRule::Autosized);
Window->SetContent(
SAssignNew(Options, SPDFImportOptions)
.WidgetWindow(Window)
.ImportOptions(Result)
.Filename(FText::FromString(Filename))
);
TSharedPtr<SWindow> ParentWindow;
if (FModuleManager::Get().IsModuleLoaded("MainFrame"))
{
IMainFrameModule& MainFrame = FModuleManager::LoadModuleChecked<IMainFrameModule>("MainFrame");
ParentWindow = MainFrame.GetParentWindow();
}
FSlateApplication::Get().AddModalWindow(Window, ParentWindow, false);
}
UIの表示を行うShowImportOptionWindow
関数を定義します。
まずUIを表示するウィンドウのタイトルなどを設定します。
次にSWindow::SetContent
内でSPDFImportOptions
に渡す値を設定します。
次に「UnrealC++でゲームとエディタで使えるファイルピッカーを作る」でもやったようにエディタ画面のウィンドウを取得します。
最後に、FSlateApplication::Get().AddModalWindow
関数でウィンドウの表示を行います。
ウィンドウのタイトルを変えたい場合は.Title(LOCTEXT("~~","~~"))
の2番目の文字列を編集してください。
UObject* UPDFFactory::FactoryCreateFile(
UClass* InClass,
UObject* InParent,
FName InName,
EObjectFlags Flags,
const FString& Filename,
const TCHAR* Parms,
FFeedbackContext* Warn,
bool& bOutOperationCanceled
)
{
TSharedPtr<SPDFImportOptions> Options;
UPDFImportOptions* Result = NewObject<UPDFImportOptions>();
ShowImportOptionWindow(Options, Filename, Result);
if (Options->ShouldImport())
{
UPDF* NewPDF = CastChecked<UPDF>(StaticConstructObject_Internal(InClass, InParent, InName, Flags));
UPDF* LoadedPDF = GhostscriptCore->ConvertPdfToPdfAsset(Filename, Result->Dpi, Result->FirstPage, Result->LastPage, true);
if (LoadedPDF != nullptr)
{
NewPDF->PageRange = LoadedPDF->PageRange;
NewPDF->Dpi = LoadedPDF->Dpi;
NewPDF->Pages = LoadedPDF->Pages;
NewPDF->Filename = Filename;
NewPDF->TimeStamp = IFileManager::Get().GetTimeStamp(*Filename);
NewPDF->AssetImportData = NewObject<UAssetImportData>();
NewPDF->AssetImportData->SourceData.Insert({ NewPDF->Filename, NewPDF->TimeStamp });
}
return NewPDF;
}
else
{
bOutOperationCanceled = true;
return nullptr;
}
}
最後にFactoryCreateFile
関数内でShowImportOptionWindow
関数を呼び出して値を取得してアセットに適用します。
キャンセルボタンが押された時に、引数のbOutOperationCanceled
をtrueにするのを忘れるとキャンセルした時にエラーが出るので気を付けましょう。
#おわりに
自作アセットの作り方については色んな方が解説されていますが、インポートオプションの表示については情報がなかったため記事を書いてみました。説明が長い上にSlateにはノータッチなので分かりにくい記事になってしまったかもしれませんが、分からないことがあればコメントかTwitterまでご連絡頂ければわかる範囲で対応することができます。
この記事が、自作アセットにインポートオプションを付けようとしている方の助けになれば幸いです。