パソコンでプログラミングしよう ウィンドウズC++プログラミング環境の構築
1.6.3.6(15)
pimplイディオムの導入(2)

KTxtEditプロジェクトのpimplイディオムを深化してメインウィンドウGUIを他のクラスから隠蔽する。

前ステップでpimplイディオムを導入してインターフェースとロジック実装を分離したが、さらにメインウィンドウのGUIを隠蔽することでクラス間の依存性を大幅に少なくする。

プログラミング手順

  1. wxSmith導入コントロールを削除する
  2. コントロールをImplクラスにマニュアル実装する
  3. リポジトリ更新とビルドテスト

wxSmith導入コントロールを削除する

wxSmithはKTxtEditFrameクラスコントラクタの//(*Initialize(KTxtEditFrame) ... //*)などにコントロール生成コードを挿入する。wxSmithエディタでコントロールを削除するとこれらのコードも削除してしまうので、ロジック実装(Impl)クラスにマニュアル実装する際の参考用に任意の形で保存しておく。Git for Windowsリポジトリがあればその履歴でも良い。

wxSmithエディタでテキストコントロール(wxTextCtrl)をマウス選択しキーボードDelキー、あるいは画面右側[Delete current selection]ツールボタンで削除する。wxSmithエディタ上部ツールホルダーパネルのメニューバー(wxMenuBar)アイコンをマウス選択して削除する。ステータスバー(wxStatusBar)も同様に削除する。メインウィンドウクローズイベント(wxEVT_CLOSE_WINDOW)もwxSmithで登録削除する。[Management]ウィンドウ[Resources]ページ(wxSmithリソースマネージャ)のツリービューで[Resources|KTxtEdit|wxFrame|KTxtEditFrame|wxFrame]ノードを選択、下部プロパティエディタをイベントエディタに切り替え、EVT_CLOSEハンドラを-- None --に戻す。

KTxtEditFrame.hおよびKTxtEditFrame.cppのwxSmith挿入コードの大部分が削除されているのを確認する。マニュアル実装したアイドル時イベント(wxEVT_IDLE)ハンドラ登録は残り、これも削除する。イベントハンドラであったKTxtEditFrameクラスのメンバ関数(OnNewなど)は自動削除されないため、インクルード、ソースコード両方から全て削除する。結果としてKTxtEditFrameはコンストラクタ/デストラクタのみが残りGUIはクラス外から完全に隠蔽される。

覚え書き
unique_ptrでpimplイディオムを実装してるため、KTxtEditFrameデストラクタは本体空だが削除できない。

KTxtEditFrame.h

class KTxtEditFrame: public wxFrame
{
public:
KTxtEditFrame(wxWindow* parent,wxWindowID id = -1);
virtual ~KTxtEditFrame();
private:
//(*Handlers(KTxtEditFrame)
//*)
//(*Identifiers(KTxtEditFrame)
//*)
//(*Declarations(KTxtEditFrame)
//*)
MY_PIMPL(std::unique_ptr,Impl,pimpl_);
DECLARE_EVENT_TABLE()
};

KTxtEditFrame.cpp

...
KTxtEditFrame::KTxtEditFrame(wxWindow* parent,wxWindowID id):pimpl_{nullptr}
{
//(*Initialize(KTxtEditFrame)
Create(parent, id, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_STYLE, _T("id"));
//*)
pimpl_.reset(new Impl{this});
}
KTxtEditFrame::~KTxtEditFrame()
{
//(*Destroy(KTxtEditFrame)
//*)
}

コントロールをImplクラスにマニュアル実装する

保存しておいたwxSmith挿入コードを参考にしながらKTxtEditFrame::Implクラスのコンストラクタにコントロール生成コードをマニュアル実装する。メニュー項目(wxMenuItem)はアイドル時イベント(wxEVT_IDLE)ハンドラ(OnIdle)で有効/無効を切り替えるためmenuItemSave_メンバ変数などでポインタ保持する。Implクラスでイベントハンドラを実装するメンバ関数(OnNewなど)はKTxtEditFrameクラスのイベントハンドラからコールされていたが、これらを直接のイベントハンドラとして登録する。なおImplクラスはwxEvtHandler派生でないためwxEvtHandler::Connectは使えず、wxEvtHandler::Bindメンバ関数で登録する。これらのメンバ関数はOnIdle以外に変更は無いが、外部からコールされなくなったためプライベートに変更する。

KTxtEditFrame.cpp

class KTxtEditFrame::Impl
{
private:
KTxtEditFrame* const form_;
wxTextCtrl* const textCtrl_;
wxStatusBar* const statusBar_;
wxHelpController helpController_;
wxString fileFullPath_;
wxMenuItem* menuItemSave_;
wxMenuItem* menuItemSaveAs_;
wxMenuItem* menuItemUndo_;
wxMenuItem* menuItemRedo_;
wxMenuItem* menuItemCut_;
wxMenuItem* menuItemCopy_;
wxMenuItem* menuItemPaste_;
private:
bool ConfirmIfModified() const
{
return !textCtrl_->IsModified()
||wxMessageBox(_("The current file is modified. Do you want to discard?")
,MYAPPINFO_NAME,wxICON_QUESTION|wxYES_NO|wxNO_DEFAULT|wxCENTER)==wxYES;
}
wxMenuBar* CreateMenuBar()
{
auto* menuBar=new wxMenuBar{};
auto* menuFile=new wxMenu{};menuBar->Append(menuFile,_("&File"));
auto* menuEdit=new wxMenu{};menuBar->Append(menuEdit,_("&Edit"));
auto* menuHelp=new wxMenu{};menuBar->Append(menuHelp,_("&Help"));
auto* menuItemNew=new wxMenuItem(menuFile,wxID_ANY,_("&New"),_("Create a new file"),wxITEM_NORMAL);
menuFile->Append(menuItemNew);
auto* menuItemOpen=new wxMenuItem(menuFile,wxID_ANY,_("&Open...\tCtrl-O"),_("Open a file"),wxITEM_NORMAL);
menuFile->Append(menuItemOpen);
auto* menuItemSave=new wxMenuItem(menuFile,wxID_ANY,_("&Save\tCtrl-S"),_("Save the current file"),wxITEM_NORMAL);
menuFile->Append(menuItemSave);
auto* menuItemSaveAs=new wxMenuItem(menuFile,wxID_ANY,_("Save &as..."),_("Save the current file with a different name"),wxITEM_NORMAL);
menuFile->Append(menuItemSaveAs);
menuFile->AppendSeparator();
auto* menuItemQuit=new wxMenuItem(menuFile,wxID_ANY,_("&Quit\tAlt-F4"),_("Quit the application"),wxITEM_NORMAL);
menuItemQuit->SetBitmap(bitmapQuit_XPM);
menuFile->Append(menuItemQuit);
auto* menuItemUndo=new wxMenuItem(menuEdit,wxID_ANY,_("&Undo\tCtrl-Z"),_("Undo the last editing operation"),wxITEM_NORMAL);
menuEdit->Append(menuItemUndo);
auto* menuItemRedo=new wxMenuItem(menuEdit,wxID_ANY,_("&Redo\tCtrl-Y"),_("Redo the last editing operation"),wxITEM_NORMAL);
menuEdit->Append(menuItemRedo);
menuEdit->AppendSeparator();
auto* menuItemCut=new wxMenuItem(menuEdit,wxID_ANY,_("Cu&t\tCtrl-X"),_("Cut selected text to clipboard"),wxITEM_NORMAL);
menuEdit->Append(menuItemCut);
auto* menuItemCopy=new wxMenuItem(menuEdit,wxID_ANY,_("&Copy\tCtrl-C"),_("Copy selected text to clipboard"),wxITEM_NORMAL);
menuEdit->Append(menuItemCopy);
auto* menuItemPaste=new wxMenuItem(menuEdit,wxID_ANY,_("&Paste\tCtrl-V"),_("Paste text from clipboard"),wxITEM_NORMAL);
menuEdit->Append(menuItemPaste);
auto* menuItemHelp=new wxMenuItem(menuHelp,wxID_ANY,_("&Help\tF1"),_("Show help file of this application"),wxITEM_NORMAL);
menuItemHelp->SetBitmap(bitmapHelp_XPM);
menuHelp->Append(menuItemHelp);
auto* menuItemAbout=new wxMenuItem(menuHelp,wxID_ANY,_("&About"),_("Show info about this application"), wxITEM_NORMAL);
menuItemAbout->SetBitmap(bitmapAbout_XPM);
menuHelp->Append(menuItemAbout);
form_->Bind(wxEVT_MENU,&Impl::OnNew,this,menuItemNew->GetId());
form_->Bind(wxEVT_MENU,&Impl::OnOpen,this,menuItemOpen->GetId());
form_->Bind(wxEVT_MENU,&Impl::OnSave,this,menuItemSave->GetId());
form_->Bind(wxEVT_MENU,&Impl::OnSaveAs,this,menuItemSaveAs->GetId());
form_->Bind(wxEVT_MENU,&Impl::OnQuit,this,menuItemQuit->GetId());
form_->Bind(wxEVT_MENU,&Impl::OnUndo,this,menuItemUndo->GetId());
form_->Bind(wxEVT_MENU,&Impl::OnRedo,this,menuItemRedo->GetId());
form_->Bind(wxEVT_MENU,&Impl::OnCut,this,menuItemCut->GetId());
form_->Bind(wxEVT_MENU,&Impl::OnCopy,this,menuItemCopy->GetId());
form_->Bind(wxEVT_MENU,&Impl::OnPaste,this,menuItemPaste->GetId());
form_->Bind(wxEVT_MENU,&Impl::OnHelp,this,menuItemHelp->GetId());
form_->Bind(wxEVT_MENU,&Impl::OnAbout,this,menuItemAbout->GetId());
menuItemSave_=menuItemSave;
menuItemSaveAs_=menuItemSaveAs;
menuItemUndo_=menuItemUndo;
menuItemRedo_=menuItemRedo;
menuItemCut_=menuItemCut;
menuItemCopy_=menuItemCopy;
menuItemPaste_=menuItemPaste;
return menuBar;
}
public:
Impl(KTxtEditFrame* form):form_{form}
,textCtrl_{new wxTextCtrl(form_,wxID_ANY,wxEmptyString
,wxDefaultPosition,wxDefaultSize,wxTE_MULTILINE,wxDefaultValidator,wxTextCtrlNameStr)}
,statusBar_{new wxStatusBar{form_,wxID_ANY,wxSTB_DEFAULT_STYLE,wxStatusBarNameStr}}
,helpController_{},fileFullPath_{}
{
form_->SetMenuBar(CreateMenuBar());
statusBar_->SetFieldsCount(2,std::array<int,2>{-1,50}.data());
statusBar_->SetStatusStyles(2,std::array<int,2>{wxSB_NORMAL,wxSB_NORMAL}.data());
form_->SetStatusBar(statusBar_);
form_->Bind(wxEVT_IDLE,&Impl::OnIdle,this);
form_->Bind(wxEVT_CLOSE_WINDOW,&Impl::OnClose,this);
auto iconBundle=wxIconBundle{};
iconBundle.AddIcon(wxIcon{"aaaa",wxICON_DEFAULT_TYPE,16,16});
iconBundle.AddIcon(wxIcon{"aaaa",wxICON_DEFAULT_TYPE,32,32});
form_->SetIcons(iconBundle);
helpController_.Initialize
(wxFileName{wxStandardPaths::Get().GetDataDir(),MYAPPINFO_NAME ".chm"}
.GetFullPath());
}
private:
void OnNew(wxCommandEvent& event)
{
if (ConfirmIfModified())
{
textCtrl_->Clear();
}
}
...
void OnIdle(wxIdleEvent& event)
{
form_->SetTitle(wxString::Format("%s%s - " MYAPPINFO_NAME
,textCtrl_->IsModified()?"*":""
,fileFullPath_.IsEmpty()?_("(Unnamed)"):fileFullPath_));
auto x=long{};auto y=long{};
textCtrl_->PositionToXY(textCtrl_->GetInsertionPoint(),&x,&y);
statusBar_->SetStatusText(wxString::Format("%d:%d",y+1,x+1),1);
menuItemSave_->Enable(textCtrl_->IsModified());
menuItemUndo_->Enable(textCtrl_->CanUndo());
menuItemRedo_->Enable(textCtrl_->CanRedo());
menuItemCut_->Enable(textCtrl_->CanCut());
menuItemCopy_->Enable(textCtrl_->CanCopy());
menuItemPaste_->Enable(textCtrl_->CanPaste());
event.Skip();
}
void OnClose(wxCloseEvent& event)
{
if (event.CanVeto()&&!ConfirmIfModified())
{
event.Veto();
}
else
{
event.Skip();
}
}
};
覚え書き
wxSmithはメニュー項目選択イベントとして古いwxEVT_COMMAND_MENU_SELECTEDを挿入コードに用いるが、この機会にwxEVT_MENUに書き換えている。両者は同じでどちらを用いても構わない。

リポジトリ更新とビルドテスト

動作確認(4)を参考にGit for Windowsリポジトリを更新する。必要ならビルド実行する。

UMLクラス図

簡略したUMLクラス図でアプリケーションの構成を説明する。

  • KTxtEditFrameのイベントハンドラをそのロジック実装クラス(KTxtEditFrame::Impl)へ移す。
  • テキストコントロール(wxTextCtrl)、メニューバー(wxMenuBar)、ステータスバー(wxStatusBar)もKTxtEditFrame::Implが生成所有すると見なす。