パソコンでプログラミングしよう ウィンドウズC++プログラミング環境の構築
1.6.3.6(15)
ファイル入出力クラスの追加(2)

KTxtEditプロジェクトにファイル入出力クラスを追加して複数文字コードに対応する。

複数文字コードに対応するファイル入出力クラス(TMyFileOpenSave)を作成する。その実装は前ステップで作成した文字コードリストクラス(TMyEncodings)とヒープストリームバッファ(TMyLocalAllocOStreamBuf/TMyLocalAllocIStreamBuf)を利用する。

プログラミング手順

  1. ファイル入出力クラス(TMyFileOpenSave)
  2. コア実装クラス(TMyCoreImpl)の修正
  3. メインウィンドウクラス(KTxtEditFrame)の修正
  4. リポジトリ更新とビルドテスト

ファイル入出力クラス(TMyFileOpenSave)

TMyFileOpenSave.h/TMyFileOpenSave.cppにTMyFileOpenSaveクラススケルトンを作成してこれを修正する。TMyFileOpenSaveはpimplイディオムで実装する。

TMyFileOpenSaveはwxTextCtrlのエディットコントロールヒープに直接アクセスしてファイルとデータ交換する。エディットコントロールヒープは文字コードUTF-16、改行コードCRLFでマイクロソフト標準に従う。これをワイド文字(wchar_t)で扱えばバイトオーダーは関係しないが、あえてバイト列表現すればインテル系アーキテクチャのリトルエンディアンでバイトオーダーマークを持たない。一方でファイルは文字コード、バイトオーダーマーク有無、改行コードをオプション指定する。ファイルの外部表現はバイト列で16ビット/32ビット符号単位の文字コードはバイトオーダーの明示を必要とする。

ロジック実装クラス(TMyFileOpenSave::Impl)のDoOpen/DoSaveプライベートメンバ関数がデータ交換の要で、ファイルストリームバッファ(C++標準ライブラリwfilebuf)とヒープストリームバッファ(TMyLocalAllocOStreamBuf/TMyLocalAllocIStreamBuf)がワイド文字を値とするイテレータでデータ交換する。wfilebufロケールのcodecvtファセットが文字コードを相互変換する。wfilebufはバイナリモードで開いてファイル改行コードは変換せず、TMyLocalAllocOStreamBuf/TMyLocalAllocIStreamBufが改行コードを相互変換する。バイトオーダーマークはDoOpen/DoSaveがwfilebufを開いた直後に必要に応じて処理する。

TMyFileOpenSaveはファイル履歴機能も実装する。Implはファイル履歴をTMyFileList(std::list<TMyFile>)型recencies_メンバ変数に記憶する。TMyFile構造体はファイルパス、TMyFileOption構造体、ID値からなる。recencies_はID値を除きレジストリに保存するためTMyRegKeyクラスDoSet/DoGetメンバ関数のTMyFileList型による明示的特殊化を追加する。ネストクラス(クラススコープで定義されるクラス)RecentFilesMenuはwxMenu派生で、ファイル履歴を表示する[File|Recent files]サブメニューを実装する。recencies_をメニュー項目に展開してそのハンドラでファイルを読み込むが、各メニュー項目ID値にTMyFile構造体ID値を代入してハンドラでのTMyFile検索を容易とする。

ファイル履歴の最大数はオプション設定ダイアログが設定する。ImplはTMyOptionDialogのオブザーバーでオプション設定を変更するとUpdateメンバ関数を実行し、履歴最大数がrecencies_要素数より小さくなる場合に要素数を縮小する。Implは同時にRecentFilesMenuをオブザーバーとする(オブザーバーデザインパターンの)サブジェクトで、recencies_を履歴追加や要素数縮小で変更するとRecentFilesMenuのUpdateメンバ関数をコールしてrecencies_の変更を反映する。なおImplはオブザーバーRecentFilesMenuをクラス内に定義するため、サブジェクトとしてのメンバ関数Notify、Attach、Dettachは全てプライベートとした。

CreateRecentFilesMenuメンバ関数はRecentFilesMenuを構築する。[Files|Recent files]サブメニューが今のところ唯一のインスタンスだが複数を可能としている。[Files|Recent files]サブメニューはメインウィンドウ(KTxtEditFrame)インスタンスが所有するので、CreateRecentFilesMenuはTMyCoreImplクラスまで露出する。

[File|Open]、[File|Save]で表示するファイル選択ダイアログはwxWidgetsライブラリのwxFileDialogを用いるが、文字コード、バイトオーダーマーク、ファイル改行コードを同時指定するためwxPanel派生パネルをダイアログに追加する。ImplネストクラスOptionPanelがそのパネルで、OptionPanel::CreateInstanceスタティックメンバ関数を生成関数ポインタとしてwxFileDialog::SetExtraControlCreatorメンバ関数の実引数に渡す。OptionPanelはImplを参照するがwxFileDialog表示まで構築されないためImplインスタンスポインタ(または参照)を直接渡す手段が無く、グローバル変数あるいはスタティックメンバ変数を用いざるを得ない。スタティックメンバ変数tHis_がそういったImplインスタンスポインタで、コンストラクタ/デストラクタで設定/解除する。

メンバ関数 機能 返値 説明 仮引数 説明
TMyFileOpenSave コンストラクタ - - wxTextCtrl* テキストコントロール
Open ダイアログ表示してファイルを開く bool ファイルを開いた - -
Save ファイルを上書き保存する bool ファイルを保存した - -
SaveAs ダイアログ表示してファイルに名前を付けて保存 bool ファイルを保存した - -
UnnameCurrent ファイル名とオプションの定義をクリアする void - - -
CurrentIsUnnamed ファイル名とオプションが定義されているか bool 定義されている - -
GetCurrentPath 現在のファイル名を取得 const wxString& ファイルフルパス名 - -
GetCurrentOption 現在のオプションを取得 const TMyFileOption& 文字コード、BOM、改行コード - -
CreateRecentFilesMenu ファイル履歴サブメニュー作成 wxMenu* [File|Recent files]など - -

TMyFileOpenSave.h

#ifndef TMYFILEOPENSAVE_H
#define TMYFILEOPENSAVE_H
#include "MyInterfaceType.h"
class TMyFileOpenSave final
{
private:
MY_PIMPL(std::unique_ptr,Impl,pimpl_);
public:
explicit TMyFileOpenSave(wxTextCtrl* textCtrl);
TMyFileOpenSave(const TMyFileOpenSave&)=delete;
TMyFileOpenSave& operator=(const TMyFileOpenSave&)=delete;
~TMyFileOpenSave();
bool Open();
bool Save();
bool SaveAs();
void UnnameCurrent();
bool CurrentIsUnnamed() const;
const wxString& GetCurrentPath() const;
const TMyFileOption& GetCurrentOption() const;
wxMenu* CreateRecentFilesMenu();
};
#endif // TMYFILEOPENSAVE_H

TMyFileOpenSave.cpp

#include "wx_pch.h"
#include "TMyFileOpenSave.h"
#include "TMyLocalAllocStreamBuf.h"
#include "TMyRegKey.h"
#include "version_macro.h"
#include "MyUtility.h"
#include "TMyEncodings.h"
#include "TMyOptionDialog.h"
#include <fstream>
#include <list>
#include <set>
#include <algorithm>
namespace
{
struct TMyFile
{
wxString path_;
TMyFileOption option_;
int id_;
};
using TMyFileList=std::list<TMyFile>;
}
template<> bool TMyRegKey::DoSet(const wxString& name,const TMyFileList& val)
{
auto buf=wxMemoryBuffer{};
auto dataSize=size_t{0};
for (const auto& r:val)
{
dataSize+=(r.path_.Length()+1)*sizeof(wchar_t)
+(r.option_.encoding_.Length()+1)*sizeof(wchar_t)+sizeof(bool)+sizeof(TMyFileOption::EOL);
}
if (dataSize>buf.GetBufSize()) {buf.SetBufSize(dataSize);}
buf.SetDataLen(dataSize);
auto* pos=static_cast<char*>(buf.GetData());
for (const auto& r:val)
{
pos=reinterpret_cast<char*>
(std::copy(r.path_.begin(),r.path_.end(),reinterpret_cast<wchar_t*>(pos)));
*reinterpret_cast<wchar_t*>(pos)=0;pos+=sizeof(wchar_t);
pos=reinterpret_cast<char*>
(std::copy(r.option_.encoding_.begin(),r.option_.encoding_.end(),reinterpret_cast<wchar_t*>(pos)));
*reinterpret_cast<wchar_t*>(pos)=0;pos+=sizeof(wchar_t);
*reinterpret_cast<bool*>(pos)=r.option_.byteOrderMark_;pos+=sizeof(bool);
*reinterpret_cast<TMyFileOption::EOL*>(pos)=r.option_.endOfLine_;pos+=sizeof(TMyFileOption::EOL);
}
return Set(name,buf);
}
template<> bool TMyRegKey::DoGet(const wxString& name,TMyFileList& val) const
{
auto buf=wxMemoryBuffer{};
if (Get(name,buf))
{
auto* pos=static_cast<char*>(buf.GetData());
auto* posEnd=pos+buf.GetDataLen();
while (pos<posEnd)
{
auto r=TMyFile{};
r.path_=reinterpret_cast<wchar_t*>(pos);pos+=(r.path_.Length()+1)*sizeof(wchar_t);
r.option_.encoding_=reinterpret_cast<wchar_t*>(pos);pos+=(r.option_.encoding_.Length()+1)*sizeof(wchar_t);
r.option_.byteOrderMark_=*reinterpret_cast<bool*>(pos);pos+=sizeof(bool);
r.option_.endOfLine_=*reinterpret_cast<TMyFileOption::EOL*>(pos);pos+=sizeof(TMyFileOption::EOL);
r.id_=wxNewId();
val.push_back(r);
}
return true;
}
return false;
}
class TMyFileOpenSave::Impl:public TMyOptionDialog::Observer
{
private:
class Observer
{
private:
Impl& subject_;
public:
virtual void Update(const Impl& subject)=0;
protected:
Observer(Impl& subject):subject_{subject} {subject_.Attach(this);}
~Observer() {subject_.Dettach(this);}
};
private:
class RecentFilesMenu;
class OptionPanel;
private:
static Impl* tHis_;
static constexpr wchar_t bom_=L'\uFEFF';
wxTextCtrl* const textCtrl_;
TMyFileList recencies_;
TMyFile current_;
std::set<Observer*> observers_;
TMyRegValues regValues_;
private:
void Notify() {for (auto* p:observers_) {p->Update(*this);}}
void Attach(Observer* observer) {observers_.insert(observer);}
void Dettach(Observer* observer) {observers_.erase(observer);}
private:
static Impl& GetImplRef() {assert(tHis_);return *tHis_;}
static TMyFileOption GetDefaultOption() {return TMyOptionDialog::GetInstance().GetDefaultFileOption();}
static TMyFile GetUnnamed() {return {wxEmptyString,GetDefaultOption(),wxID_NONE};}
private:
void Remember(const wxString& path,const TMyFileOption& option)
{
auto p=std::find_if(recencies_.begin(),recencies_.end()
,[path](const auto& r){return path.IsSameAs(r.path_);});
if (p==recencies_.end())
{
recencies_.push_front({path,option,wxNewId()});
auto maxSize=static_cast<size_t>(TMyOptionDialog::GetInstance().GetRecentFilesMax());
if (recencies_.size()>maxSize) {recencies_.resize(maxSize);}
}
else
{
p->option_=option;
recencies_.splice(recencies_.begin(),recencies_,p);
}
current_=recencies_.front();
Notify();
}
bool GetOption(const wxString& path,TMyFileOption& option) const
{
auto p=std::find_if(recencies_.cbegin(),recencies_.cend()
,[path](const auto& r){return path.IsSameAs(r.path_);});
if (p==recencies_.cend()) {option=GetDefaultOption();return false;}
else {option=p->option_;return true;}
}
bool DoOpen(const wxString& path,const TMyFileOption& option)
{
try
{
auto in=std::wfilebuf{};
in.open(path.wc_str(),std::ios_base::in|std::ios_base::binary);
in.pubimbue(std::locale{std::locale{},TMyEncodings::GetInstance().GetCodeCvt(option.encoding_)});
if (option.byteOrderMark_)
{
if (in.sbumpc()!=bom_)
{throw std::runtime_error{"No byte order mark that is expected."};}
}
else
{
if (in.sgetc()==bom_)
{throw std::runtime_error{"Encountered unexpected byte order mark."};}
}
auto out=TMyLocalAllocOStreamBuf{option.endOfLine_};
std::copy
(std::istreambuf_iterator<wchar_t>{&in}
,std::istreambuf_iterator<wchar_t>{}
,std::ostreambuf_iterator<wchar_t>{&out});
auto hnd=out.Replace(reinterpret_cast<HLOCAL>(::SendMessage(textCtrl_->GetHWND(),EM_GETHANDLE,0,0)));
::SendMessage(textCtrl_->GetHWND(),EM_SETHANDLE,reinterpret_cast<WPARAM>(hnd),0);
Remember(path,option);
return true;
}
catch (std::exception& e)
{
wxMessageBox(e.what(),MYAPPINFO_NAME,wxCENTER|wxICON_ERROR);
return false;
}
}
bool DoSave(const wxString& path,const TMyFileOption& option)
{
try
{
auto out=std::wfilebuf{};
out.open(path.wc_str(),std::ios_base::out|std::ios_base::binary);
out.pubimbue(std::locale{std::locale{},TMyEncodings::GetInstance().GetCodeCvt(option.encoding_)});
if (option.byteOrderMark_) {out.sputc(bom_);}
auto hnd=reinterpret_cast<HLOCAL>(::SendMessage(textCtrl_->GetHWND(),EM_GETHANDLE,0,0));
auto in=TMyLocalAllocIStreamBuf{hnd,option.endOfLine_};
std::copy
(std::istreambuf_iterator<wchar_t>{&in}
,std::istreambuf_iterator<wchar_t>{}
,std::ostreambuf_iterator<wchar_t>{&out});
Remember(path,option);
return true;
}
catch (std::exception& e)
{
wxMessageBox(e.what(),MYAPPINFO_NAME,wxCENTER|wxICON_ERROR);
return false;
}
}
bool DialogExecute(wxWindow* parent,const wxString& message,long style
,wxString& path,TMyFileOption& option) const;
public:
Impl(wxTextCtrl* textCtrl):textCtrl_{textCtrl}
,recencies_{},current_{GetUnnamed()},observers_{}
,regValues_{wxRegKey::HKCU,"Software" "\\" MYAPPINFO_PUBLISHER "\\" MYAPPINFO_NAME "\\" "TMyFileOpenSave"}
{
assert(!tHis_);tHis_=this;
regValues_.AddValueSet
([this](TMyFileList& recencies)
{
recencies=recencies_;
}
,[this](const TMyFileList& recencies)
{
recencies_=recencies;
}
,TMyRegValues::KeyAndDefault<TMyFileList>{"Recencies",recencies_});
regValues_.GetAll();
}
~Impl() {tHis_=nullptr;}
bool Open()
{
auto path=wxString{};
auto option=TMyFileOption{};
return DialogExecute(textCtrl_,_("Open a file"),wxFD_OPEN|wxFD_FILE_MUST_EXIST,path,option)
&&DoOpen(path,option);
}
bool Save()
{
if (current_.id_==wxID_NONE) {return SaveAs();}
else {return DoSave(current_.path_,current_.option_);}
}
bool SaveAs()
{
auto path=wxString{};
auto option=TMyFileOption{};
return DialogExecute(textCtrl_,_("Save the current file"),wxFD_SAVE|wxFD_OVERWRITE_PROMPT,path,option)
&&DoSave(path,option);
}
void UnnameCurrent() {current_=GetUnnamed();}
bool CurrentIsUnnamed() const {return current_.id_==wxID_NONE;}
const wxString& GetCurrentPath() const {return current_.path_;}
const TMyFileOption& GetCurrentOption() const {return current_.option_;}
wxMenu* CreateRecentFilesMenu();
void Update(const TMyOptionDialog& subject) override
{
auto maxSize=static_cast<size_t>(subject.GetRecentFilesMax());
if (recencies_.size()>maxSize) {recencies_.resize(maxSize);Notify();}
}
};
TMyFileOpenSave::Impl* TMyFileOpenSave::Impl::tHis_=nullptr;
class TMyFileOpenSave::Impl::RecentFilesMenu:public wxMenu,public Observer
{
private:
using MenuCallBack=std::function<void(const wxString&,const TMyFileOption& option)>;
const TMyFileList& recencies_;
const MenuCallBack callback_;
TMyEventHandlerManager evtHandlerManager_;
void OnMenuItem(wxCommandEvent& event)
{
auto p=std::find_if
(recencies_.begin(),recencies_.end(),[&event](const auto& r){return r.id_==event.GetId();});
assert(p!=recencies_.end());
callback_(p->path_,p->option_);
}
public:
RecentFilesMenu(Impl& subject,MenuCallBack callback)
:wxMenu{},Observer{subject}
,recencies_{subject.recencies_},callback_{callback},evtHandlerManager_{this}
{
evtHandlerManager_.Add(wxEVT_MENU,&RecentFilesMenu::OnMenuItem,this);
Update(subject);
}
void Update(const Impl& subject) override
{
while (GetMenuItemCount()) {Delete(FindItemByPosition(0));}
for (const auto& r:recencies_)
{
Append(new wxMenuItem{this,r.id_,r.path_,_("Open a recent file")});
}
if (GetMenuItemCount()==0)
{
auto* dummyItem=new wxMenuItem{this,wxID_NONE,_("(Empty)")};
dummyItem->Enable(false);
Append(dummyItem);
}
}
};
class TMyFileOpenSave::Impl::OptionPanel:public wxPanel
{
private:
using EOL=TMyFileOption::EOL;
static constexpr int headMargin_=150;
static constexpr int labelWidth_=100;
private:
wxFileDialog* const dlg_;
wxString const labelEncoding_;
wxString const labelEncodingRemembered_;
wxStaticText* const staticTextEncoding_;
wxChoice* const choiceEncoding_;
wxCheckBox* const checkBoxByteOrderMark_;
wxChoice* choiceEndOfLine_;
TMyFileOption option_;
TMyEventHandlerManager evtHandlerManager_;
private:
// Some extra controls embedded in wxFileDialog by wxFileDialog::SetExtraControlCreator would be
// invalid after wxFileDialog::ShowModal returns. Preserve their states by catching change events.
void OnChange(wxCommandEvent& event)
{
option_.encoding_=choiceEncoding_->GetStringSelection();
checkBoxByteOrderMark_->Enable(choiceEncoding_->GetClientData(choiceEncoding_->GetCurrentSelection()));
option_.byteOrderMark_=checkBoxByteOrderMark_->IsEnabled()&&checkBoxByteOrderMark_->IsChecked();
option_.endOfLine_=static_cast<EOL>(reinterpret_cast<intptr_t>
(choiceEndOfLine_->GetClientData(choiceEndOfLine_->GetSelection())));
staticTextEncoding_->SetLabel(labelEncoding_);
}
void OnUpdateUI(wxUpdateUIEvent& event)
{
if (dlg_->GetWindowStyle()&wxFD_OPEN)
{
auto remembered=GetImplRef().GetOption(dlg_->GetCurrentlySelectedFilename().c_str(),option_);
staticTextEncoding_->SetLabel(remembered?labelEncodingRemembered_:labelEncoding_);
SyncCtrls();
}
event.Skip();
}
void SyncCtrls()
{
choiceEncoding_->SetStringSelection(option_.encoding_);
checkBoxByteOrderMark_->SetValue(option_.byteOrderMark_);
checkBoxByteOrderMark_->Enable(choiceEncoding_->GetClientData(choiceEncoding_->GetCurrentSelection()));
for (auto n=(unsigned int){0};n<choiceEndOfLine_->GetCount();++n)
{
if (reinterpret_cast<void*>(option_.endOfLine_)==choiceEndOfLine_->GetClientData(n))
{
choiceEndOfLine_->SetSelection(n);
break;
}
}
}
public:
OptionPanel(wxWindow* parent):wxPanel{parent},dlg_{static_cast<wxFileDialog*>(GetParent())}
,labelEncoding_{_("Encoding:")},labelEncodingRemembered_{_("Encoding(*):")}
,staticTextEncoding_{new wxStaticText(this,wxID_ANY,labelEncoding_,wxDefaultPosition,{labelWidth_,-1})}
,choiceEncoding_{new wxChoice{this,wxID_ANY}}
,checkBoxByteOrderMark_{new wxCheckBox{this,wxID_ANY,_("Byte order mark")}}
,choiceEndOfLine_{new wxChoice{this,wxID_ANY,wxDefaultPosition,{50,-1}}}
,option_{GetImplRef().GetCurrentOption()}
,evtHandlerManager_{this}
{
// The constructor is called twice for each a wxDialog instance (probably a dummy)
// and a wxFileDialog instance. For the former dlg_ is a dangerous static cast that
// is thought to be never used.
auto* boxSizer=new wxBoxSizer(wxHORIZONTAL);
boxSizer->Add(headMargin_,0,1,wxALL|wxEXPAND,5);
boxSizer->Add(staticTextEncoding_,0,wxALL|wxALIGN_CENTER_VERTICAL,5);
boxSizer->Add(choiceEncoding_,0,wxALL|wxEXPAND,5);
boxSizer->Add(checkBoxByteOrderMark_,0,wxALL|wxEXPAND,5);
boxSizer->Add(choiceEndOfLine_,0,wxALL|wxEXPAND,5);
SetSizer(boxSizer);
boxSizer->SetSizeHints(this);// questionable, see SetSizeHints documentation.
TMyEncodings::GetInstance().SetItemContainerControl(choiceEncoding_);
choiceEndOfLine_->Append(_("CRLF"),reinterpret_cast<void*>(EOL::CRLF));
choiceEndOfLine_->Append(_("LF"),reinterpret_cast<void*>(EOL::LF));
choiceEndOfLine_->Append(_("CR"),reinterpret_cast<void*>(EOL::CR));
evtHandlerManager_.Add(wxEVT_CHOICE,&OptionPanel::OnChange,this);
evtHandlerManager_.Add(wxEVT_CHECKBOX,&OptionPanel::OnChange,this);
evtHandlerManager_.Add(wxEVT_UPDATE_UI,&OptionPanel::OnUpdateUI,this);
SyncCtrls();
}
const TMyFileOption& GetOption() const {return option_;}
static wxWindow* CreateInstance(wxWindow* parent) {return new OptionPanel{parent};}
};
wxMenu* TMyFileOpenSave::Impl::CreateRecentFilesMenu()
{
return new RecentFilesMenu{*this
,[this](const wxString& path,const TMyFileOption& option) {DoOpen(path,option);}};
}
bool TMyFileOpenSave::Impl::DialogExecute(wxWindow* parent,const wxString& message,long style
,wxString& path,TMyFileOption& option) const
{
wxFileDialog dlg{parent,message,wxEmptyString,wxEmptyString,wxEmptyString,style};
dlg.SetExtraControlCreator(&OptionPanel::CreateInstance);
if (dlg.GetWindowStyle()&wxFD_SAVE) {dlg.SetPath(current_.path_);}
if (dlg.ShowModal()==wxID_OK)
{
auto* optionPanel=static_cast<OptionPanel*>(dlg.GetExtraControl());
path=dlg.GetPath();
option=optionPanel->GetOption();
return true;
}
return false;
}
TMyFileOpenSave::TMyFileOpenSave(wxTextCtrl* textCtrl):pimpl_{new Impl{textCtrl}} {}
TMyFileOpenSave::~TMyFileOpenSave() {}
bool TMyFileOpenSave::Open() {return pimpl_->Open();}
bool TMyFileOpenSave::Save() {return pimpl_->Save();}
bool TMyFileOpenSave::SaveAs() {return pimpl_->SaveAs();}
void TMyFileOpenSave::UnnameCurrent() {pimpl_->UnnameCurrent();}
bool TMyFileOpenSave::CurrentIsUnnamed() const {return pimpl_->CurrentIsUnnamed();}
const wxString& TMyFileOpenSave::GetCurrentPath() const {return pimpl_->GetCurrentPath();}
const TMyFileOption& TMyFileOpenSave::GetCurrentOption() const {return pimpl_->GetCurrentOption();}
wxMenu* TMyFileOpenSave::CreateRecentFilesMenu() {return pimpl_->CreateRecentFilesMenu();}

コア実装クラス(TMyCoreImpl)の修正

TMyCoreImpl.cppに"TMyFileOpenSave.h"インクルードを追加する。ロジック実装クラス(TMyCoreImpl::Impl)にTMyFileOpenSave型fileOpenSave_メンバ変数を追加してコンストラクタ初期化リストに加える。fileFullPath_メンバ変数は不要となり削除する。New、Open、Save、SaveAsメンバ関数をfileOpenSave_を使って書き換える。タイトルバーにファイル文字コード情報などを追加するためGetTitleStringメンバ関数を変更する。[File|Recent files]サブメニューを作成するTMyFileOpenSave::CreateRecentFilesMenuメンバ関数をKTxtEditFrameにコールさせるため、TMyCoreImpl、TMyCoreImpl::ImplにCreateRecentFilesMenuメンバ関数を追加する。TMyFileOpenSaveはwxTextCtrlのエディットコントロールヒープに直接アクセスしwxTextCtrlのプロパティが同期しないので、Open、Save、SaveAsメンバ関数はwxTextCtrl::SetModifiedメンバ関数をコールする。

TMyCoreImpl.h

class TMyCoreImpl final
{
...
public:
...
wxMenu* CreateRecentFilesMenu();
};

TMyCoreImpl.cpp

#include "wx_pch.h"
#include "TMyCoreImpl.h"
#include "version_macro.h"
#include "TMyOptionDialog.h"
#include "TMyFileOpenSave.h"
class TMyCoreImpl::Impl:public TMyOptionDialog::Observer
{
private:
wxTextCtrl* textCtrl_;
TMyFileOpenSave fileOpenSave_;
...
public:
Impl(wxWindow* parent)
:textCtrl_{new wxTextCtrl(parent,wxID_ANY,wxEmptyString
,wxDefaultPosition,wxDefaultSize,wxTE_MULTILINE,wxDefaultValidator,wxTextCtrlNameStr)}
,fileOpenSave_{textCtrl_}
{
Update(TMyOptionDialog::GetInstance());
}
bool New()
{
if (ConfirmIfModified())
{
textCtrl_->Clear();
fileOpenSave_.UnnameCurrent();
}
return true;
}
bool Open()
{
if (ConfirmIfModified()&&fileOpenSave_.Open())
{
textCtrl_->SetModified(false); // Redundant.
}
return true;
}
bool Save()
{
if (fileOpenSave_.Save())
{
textCtrl_->SetModified(false);
}
return true;;
}
bool SaveAs()
{
if (fileOpenSave_.SaveAs())
{
textCtrl_->SetModified(false);
}
return true;
}
...
wxString GetTitleString() const
{
auto path=wxString{};
auto encoding=wxString{};
if (fileOpenSave_.CurrentIsUnnamed())
{
path=_("(Unnamed)");
}
else
{
auto fileOption=fileOpenSave_.GetCurrentOption();
path=fileOpenSave_.GetCurrentPath();
encoding<<" - ["<<fileOption.encoding_;
if (fileOption.byteOrderMark_) {encoding<<_("(BOM)");}
encoding<<":";
switch (fileOption.endOfLine_)
{
case TMyFileOption::EOL::CRLF:
encoding<<_("CRLF");break;
case TMyFileOption::EOL::LF:
encoding<<_("LF");break;
case TMyFileOption::EOL::CR:
encoding<<_("CR");break;
}
encoding<<"]";
}
return wxString::Format("%s%s%s - " MYAPPINFO_NAME,textCtrl_->IsModified()?"*":"",path,encoding);
}
...
wxMenu* CreateRecentFilesMenu()
{
return fileOpenSave_.CreateRecentFilesMenu();
}
};
TMyCoreImpl::TMyCoreImpl(wxWindow* parent):pimpl_{new Impl{parent}} {}
TMyCoreImpl::~TMyCoreImpl() {}
...
wxMenu* TMyCoreImpl::CreateRecentFilesMenu() {return pimpl_->CreateRecentFilesMenu();}

メインウィンドウクラス(KTxtEditFrame)の修正

ロジック実装クラス(KTxtEditFrame::Impl)のCreateMenuBarメンバ関数で[File|Save as]メニュー項目と[File|Quit]メニュー項目の間に[File|Recent files]メニュー項目を挿入する。このメニュー項目はTMyFileOpenSave::Impl::RecentFilesMenuインスタンスでTMyFileOpenSave::Implのオブザーバーであり、寿命はサブジェクトより短くなければならない。そこでKTxtEditFrame::Implのプライベートメンバ変数であるデリータカスタマイズしたスマートポインタに保持してデストラクタで暗黙に解体する。

必要なmenuFile_とrecentFilesMenuItem_を追加し、CreateMenuBarで各変数に代入する。デストラクタはこれらを用いて[File|Recent files]メニュー項目を解体する。

KTxtEditFrame.cpp

class KTxtEditFrame::Impl
{
private:
...
std::unique_ptr<wxMenuItem,std::function<void(wxMenuItem*)>> recentFilesMenuItem_;
private:
wxMenuBar* CreateMenuBar()
{
...
menuItemManager_.Append(menuFile,_("Save &as..."),_("Save the current file with a different name"),&Impl::OnSaveAs,this);
menuFile->AppendSeparator();
assert(!recentFilesMenuItem_);
recentFilesMenuItem_={menuFile->AppendSubMenu(coreImpl_.CreateRecentFilesMenu(),_("Recent files"))
,[menuFile](auto* item){menuFile->Destroy(item);}};
menuFile->AppendSeparator();
menuItemManager_.Append(menuFile,_("&Quit\tAlt-F4"),_("Quit the application"),&Impl::OnQuit,this,true,bitmapQuit_XPM);
...
}
public:
Impl(KTxtEditFrame* form):form_{form}
,statusBar_{new wxStatusBar{form_,wxID_ANY,wxSTB_DEFAULT_STYLE,wxStatusBarNameStr}}
...
,recentFilesMenuItem_{}
{
...
}
...
};
...

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

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

UMLクラス図

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

  • ファイル入出力クラス(TMyFileOpenSave)と前ステップ作成の文字コードリストクラス(TMyEncodings)を追加する。TMyEncodingsはシングルトンである。
  • TMyFileOpenSaveロジック実装クラス(TMyFileOpenSave::Impl)はテキストコントロールとファイル間のデータ交換を実装する。
  • TMyFileOpenSave::Implは基底クラス(TMyOptionDialog::Observer)を通じてTMyOptionDialogのオブザーバーに登録する。同時にTMyRegValuesをサブオブジェクトに所有してファイル履歴リストをレジストリに保存する。