スポンサーリンク

GUIアプリ製作で学ぶポリモーフィズム ~~

本当の意味での前回記事では、PCのSSDを交換して遅くなっていたPCを生き返らせたって話をしましたが、今回はまたwxWidgetsの話に戻ります(お気に入りのPCを使えるようになって、嬉しくて脱線しました)。

「前回記事」という単語は、本来ならPC記事を指す言葉ですが、この記事では特に断りがない限りは前回記事という言葉はこの記事シリーズの前回記事を表すこととします。つまり、この記事で言う所の「前回記事」は、こちらの記事ではなく、こちらの記事を表すと考えてください。

今回の記事では、この記事シリーズの前回記事と対比する箇所が多く出てくるのですが、その説明のときに、毎回「この記事シリーズの前回記事」と書くと読者の皆さんにとっても煩わしいと思ったので。

さて、今、「対比をする」とお伝えしましたが、今回の記事では前回記事のソースコードを変更したソースコードをお見せします。

その変更を解説するという形でwxWidgetsの使い方の基礎を解説していきます。

ですが、その変更を理解するにはポリモーフィズムの考え方を理解していないといけません。そのため、今回の記事は実質的には、前回記事と対比しながらポリモーフィズムを解説するというような内容になっています。

ただ、基本的なコントロール(GUIを構成する部品のことを、この記事シリーズでは「コントロール」と呼ぶことにしています)を配置する方法はこれで理解していただけると思うので、参考にしてみてくださいませ~。

スポンサーリンク

前回の確認

前回記事ではwxWidgetsを利用した最小限のアプリケーションを作りました。そして、wxWidgetsを利用したプログラムを書くときのポイントとして、次の7つを挙げ、そのうちの5番までを解説しました。

  1. main関数ではなく、wxApp::OnInit関数がエントリポイント(main関数を書かない)
  2. wxAppは抽象クラスとなっている
  3. wxIMPLEMENT_APP()に、wxAppの派生クラスを指定する
  4. wxDECLARE_APP()で、wxIMPLEMENT_APP()で使われている関数を宣言する
  5. wxFrameでウィンドウを作成する
  6. 途中でnewしたwxWidgetsのコントロール達は、途中でdeleteしなくてもいい
  7. wxWidgetsはイベント駆動型の設計になっている

残る6番は、前回記事時点ではまだ詳しく説明できないという理由から解説をせず、7番については記事が長くなりすぎるという理由と、今回の記事に回して解説した方が分かりやすいと思ったことから、今回の記事で解説することにしていました。

ちなみに、今までの記事は次の通りです。興味があれば併せてご覧ください。

  1. MSYS2でwxWidgetsを使う方法 ~GUIアプリケーション開発の第一歩~
  2. C++で最小限のGUIアプリを作る方法 ~wxWidgetsの基礎~

さて、その前回記事でお見せしたソースコードの、どこをどういう風に変更するのか、次の節で説明します。

ソースコード

変更後のソースコードは以下の通りです(前回記事から変わっていない部分もあります)。

Main.cpp

/********************************
* Main.cpp
* 最小限のプログラム
*********************************/

#include <wx/app.h>
#include "Application.hpp"

////////////////////////////////
// 実質的には main 関数の宣言
//
// リファレンスでは、アプリケーションクラスが定義されているファイルで
// 利用するように言われているが、とりあえずこれで問題が無かったことと、
// Main.cpp に書いた方が分かりやすいと感じことから、 Main.cpp に書いている。
//
// リファレンスにもある通り、
// このマクロの最後にはセミコロンを付ける必要がある。
////////////////////////////////
wxIMPLEMENT_APP(Application);

Application.hpp

/********************************
* Application.hpp
*********************************/

#pragma once

#include <wx/app.h>

class Application : public wxApp
{
private:
public:
  virtual bool OnInit();  // 初期化用の関数を virtual を付けてオーバーライド
};

// プロトタイプ宣言みたいなもの
wxDECLARE_APP( Application );

Application.cpp

#include "Application.hpp"
#include "MainFrame.hpp"

// OnInit 関数が false を返したら、イベント監視には入らず、即終了となる
bool Application::OnInit()
{
  // 親クラスの OnInit 関数で問題が起きていたら false を返し、即終了
  if( !wxApp::OnInit() ) return false;

  // ウィンドウを作る(メモリ上にオブジェクト化しただけで、画面上に表示されるわけではない)
  wxFrame* main_window = new MainFrame( wxT("Hello World!"), wxDefaultPosition, wxDefaultSize );

  // 先ほど作ったウィンドウを画面上に表示する
  main_window -> Show();

  // true を返すと、ここからイベント監視のアイドル状態が始まる。
  return true;
}

MainFrame.hpp

/********************************
* Frame.hpp
* メインウィンドウを表すフレームを定義する
*********************************/

#pragma once

#include <wx/menu.h>
#include <wx/frame.h>

// 使いたいイベントのIDは、すべてここで定義しておく
enum class EventID : unsigned int
{
  FILE_MENU_QUIT = wxID_HIGHEST + 1
};

class MainFrame : public wxFrame
{
private:

public:
  MainFrame( const wxString & title, const wxPoint & pos, const wxSize & size );
  virtual ~MainFrame();

  void onSelectedQuit( wxCommandEvent & evt );    // quit を呼び出す
  void quit( wxCloseEvent & event );
};

MainFrame.cpp

#include "MainFrame.hpp"
#include <wx/frame.h>
#include <wx/menu.h>
#include <wx/statusbr.h>

// 委譲コンストラクタで、親クラスである wxFrame に値を渡す
MainFrame::MainFrame( const wxString & title, const wxPoint & pos, const wxSize & size )
: wxFrame( nullptr, wxID_ANY, title, pos, size )
{
  /* メインウィンドウに必要な要素を作る */
  // メニューバーのファイル項目に配置する選択肢を作る。
  wxMenu* file_items = new wxMenu();   // 作った選択肢を管理するためのインスタンスを作成
  file_items -> Append( static_cast<unsigned int>(EventID::FILE_MENU_QUIT), wxT("&Quit"), wxT("Quit this application") ); // Quit という選択肢を作成

  // メニューバーを作る
  wxMenuBar* menu_bar = new wxMenuBar();  // メニューバーを管理するためのインスタンスを作成
  menu_bar -> Append( file_items, wxT("&File") ); // File というメニュー項目を作成

  SetMenuBar( menu_bar );   // このウィンドウ上のメニューバーとして、 menu_bar を表示するように伝える

  // メニューバーの Quit が押されたというイベントと onSelectedQuit 関数を結びつける。
  // Quit が押されると onSelectedQuit が呼び出されるようにする。
  Bind( wxEVT_MENU, onSelectedQuit, this, static_cast<unsigned int>(EventID::FILE_MENU_QUIT) );

  // ウィンドウの破棄と、このクラスの quit 関数を紐づける
  // ウィンドウの破棄というイベントが発生したときに、このクラスのインスタンスの quit 関数が呼び出されるようになる
  Bind( wxEVT_CLOSE_WINDOW, &MainFrame::quit, this );
}

MainFrame::~MainFrame(){}

void MainFrame::onSelectedQuit( wxCommandEvent & evt )
{
  Close( true );    // ウィンドウを閉じる
}

void MainFrame::quit( wxCloseEvent & event )
{
  Destroy();  // ウィンドウを破棄する
}

前回記事ではcppファイルとhppファイルを合わせて、合計3つのファイルになっていました。ですが今回はMainFrame.hppとMainFrame.cppという2つのファイルを追加して、合計5つのファイルとなります。

他にも変更点はありますが、それは次の節で解説するので、ここではとりあえずソースコードをコンパイル、実行してみてください。実行結果が前回記事のものと少しだけ変わっているはずです。

前回記事時点の実行結果には無かったメニューバー(ファイルとか、ヘルプとかが書いてあるバー)とステータスバー(ウィンドウ下の、少し色が違っているバー)が付かされていることが分かると思います。

変更点

実行結果が前回記事のものから変わったわけですが、当然ながら、その原因はソースコードの変更にあるわけです。ということで、この節でソースコードのどういう変更がどういう実行結果の違いを生み出すことになったのかを解説していきます。

上でも挙げた通り、修正や追加といった前回記事からの変更点がいくつかあるものの、その中でも特に強調したい変更点は次の2点です。

  1. OnInit()関数中のインスタンス作成の部分
  2. wxFrameを継承したMainFrameクラスの作成

すべて基本的にMainFrameクラスの話になります。ですが、1番目はMainFrameクラスの呼び出し側の話で、2番目はMainFrameクラスの具体的な実装の話になります。前者は外身の話、後者は中身の話というように考えると分かりやすいかもしれません。

この1番の話が冒頭でお伝えしたポリモーフィズムの話に関連してきます。ということで、さっそく1番の解説からしていきます。

まず注目していただきたいのは、OnInit()関数です(宣言の方ではなく、cppファイルに書いてある定義の方です)。ここは、前回記事では次のようになっていました。

bool Application::OnInit()
{
  ...(略)
  wxFrame* main_window = new wxFrame( nullptr, -1, wxT("Hello World!"), wxDefaultPosition, wxDefaultSize );

  ...(略)
}

しかし、今回記事では次のようになっています。

bool Application::OnInit()
{
  ...(略)
  wxFrame* main_window = new MainFrame( wxT("Hello World!"), wxDefaultPosition, wxDefaultSize );

  ...(略)
}

変更されているのは右辺だけですね。前回記事の方ではwxFrameのインスタンスを作るようになっていましたが、今回記事の方ではMainFrameクラスのインスタンスを作るように変更されています。

このMainFrameクラスはwxFrameクラスの派生クラスとなっています。これによって表示された画面が変わっているのですが、ここでポリモーフィズムの登場です。

この部分を理解するためにはポリモーフィズムを理解しなければいけませんので、ポリモーフィズムについて説明します。ポリモーフィズムという概念自体は知っているけど、具体的にどういうことかイマイチよく理解できていない方にはちょうど良い内容なのではないかと思います。

スポンサーリンク

ポリモーフィズム入門

ポリモーフィズムは、一言で言えば「生成したオブジェクトの振る舞いを動的に変更する機能」のことです。これだけではよく分からないと思うので、もう少し詳しく説明します。

OnInit()関数中の変更部分ですが、この部分は先ほどもお伝えした通り、右辺は変わらず、左辺だけが変わっています。ここで、右辺の型が変わっていないということが重要です。

「右辺で作成されたインスタンスのポインタを、wxFrame型のポインタで受けている」という点は前回記事でも今回記事でも変わっていないわけです。

ここで、OnInit()関数の視点に立って考えてみてください。

前回記事のソースコードでは、OnInit()関数はwxFrame型のポインタに対してコンストラクタを呼び出しているということになります。

今回記事のソースコードでも同様に、OnInit()関数は、wxFrame型のポインタに対してコンストラクタを呼び出しているということになります。

そして、その後の処理は変更されていません。具体的にはshow()関数を呼び出す部分は変わっていません。

つまり、OnInit()関数の視点に立ってみれば、インスタンスを生成するときの処理は前回のソースコードから変更されたけど、それ以降の関数呼び出しは変わっていないということになります。

ですが、ここでOnInit()の視点から離れて、作成されたインスタンスの視点に立って考えると、前回記事と今回記事とでは、その中身に大きな違いがあることが分かります。

つまり前回記事のソースコードでは、インスタンス内部からの視点とは、wxFrameクラス内部からの視点ということになりますが、今回のソースコードでは、MainFrameクラスの内部からの視点になります。

そのため、呼び出された側の視点からすれば、前回記事のソースコードではwxFrameクラスに定義されている関数が「あっ、呼び出されたのは俺だな」と感じることになりますし、今回のソースコードではMainFrame(wxFrameではなく)に定義されている関数が「あっ、呼び出されたのは俺だな」と感じることになります。

つまり、どの関数(今回の例で言えばshow()関数)が呼び出されたと判断するのかが変わるということですな。

呼び出される関数が違っているため、具体的な処理も違います。

さて、インスタンス内部の視点から離れて、プログラマの視点に戻って、何が起こっていたのかを整理しましょう。

まず、インスタンスを扱う側(インスタンスの持っているメンバ関数を呼び出す側)はソースコードの変更前も変更後も、作成されたインスタンスを同じように扱っていました。

しかし、実際に呼び出されたインスタンスは違っていたため、具体的な処理には違いが出ていました。

これがポリモーフィズムです。言い換えると、「呼び出し側の処理を変更することなく、そのインスタンスを変更するだけで処理を変えることができる」ということになります。ただし、インスタンス作成の部分は変更する必要はあります。

逆に言えば、インスタンス作成の部分を変更さえすれば、それ以外は全く同じまま、振る舞いを変えることができるということになります。

ポリモーフィズムによって、プログラムの実行中に振る舞いを変える例

今回のソースコードでは、最初から作成するインスタンスを決めていましたが、プログラム実行中の状態によって、作成するインスタンスを変更することもできます。

例えばApplication.cppを次のように変更してみます。

#include "Application.hpp"
#include "MainFrame.hpp"

#include <wx/msgdlg.h> // wxMessageBoxを使うために必要

// OnInit 関数が false を返したら、イベント監視には入らず、即終了となる
bool Application::OnInit()
{
  // 親クラスの OnInit 関数で問題が起きていたら false を返し、即終了
  if( !wxApp::OnInit() ) return false;

  wxFrame* main_window = nullptr;

  // ウィンドウを作る(メモリ上にオブジェクト化しただけで、画面上に表示されるわけではない)
  switch( wxApp::argc )
  {
    case 1:
      main_window = new MainFrame( wxT("Hello World!"), wxDefaultPosition, wxDefaultSize );
      break;
    case 2:
      main_window = new wxFrame( nullptr, -1, wxT("Hello World!"), wxDefaultPosition, wxDefaultSize );
      break;
    default:
      wxMessageBox( "wrong usage." );
      break;
  }

  if( main_window == nullptr ) return false;

  // 先ほど作ったウィンドウを画面上に表示する
  main_window -> Show();

  // true を返すと、ここからイベント監視のアイドル状態が始まる。
  return true;
}

このコードでは、main_windowの具体的なインスタンスをコマンドライン引数の個数によって変更しています。

ただし、ここで指定できるコマンドライン引数は「–verbose」だけです(「–help」も使えますが、ヘルプが表示されるだけで終わってしまいます)。

コマンドライン引数の事情についてはまだよく分かっていないので、詳しくは説明できません。ただ、wxAppの派生クラス(ここではApplicationクラス)に色々な処理を追加する必要があるっぽいです。コマンドライン引数の処理方法について詳しく知りたい方は、こちらこちらが参考になるのではないかと思います。

このソースコードでは、コマンドライン引数の個数argcが1の場合は今回記事の実行結果が表示されて、argcが2の場合は前回記事の実行結果が表示されるようにしています。

なので、プログラムを実行するときに–verboseを指定すると前回記事と同じ実行結果になるはずです。例えば、MSYS2上で以下のようなコマンドを実行すれば、前回記事と同じ実行結果になるはずです。

./<file_name> --verbose

ただし、<file_name>という部分は環境に合わせて変える必要があります。例えば、実行ファイルを「<ProjectName>/build/debug/calculator.exe」というように保存していて、カレントディレクトリが<ProjectName>になっていたとしたら、次のように指定する必要があります。

./build/debug/calculator.exe --verbose

大概の方はすでに分かっているでしょうが念のため。

このソースコードを見ていただければ分かる通り、インスタンス作成の部分以外は変更されていません。つまり、インスタンス作成以外は同じコードで振る舞いを変えることができています。

ここでは振る舞いを変えるための条件としてコマンドライン引数の個数を使いました。ですが、その条件はコマンドライン引数の個数である必要はありません。プログラムに書ける条件でさえあれば、好きな条件を指定することができます。

このように、プログラムの実行中に振る舞いを変えることができるというのがポリモーフィズムになります。

しかし、このように説明すると、「それ、if文やswitch文を使ってもできるではないか。なぜ、わざわざポリモーフィズムなどという厄介な仕組みを使うんだ?」と疑問に持たれる方もいらっしゃると思います。

それを解説していると長くなりすぎてしまいますし、wxWidgetsの解説からかなり外れてしまいますから、とりあえず「ポリモーフィズムを上手く使うとifやswitchを使うよりも効率的に書けるから」とだけ説明しておきます。

以上が、wxWidgetsプログラムを例に取って説明した「ポリモーフィズムの一般論」になります。

ですが、この記事はあくまでもwxWidgetsの解説記事です。ですから、軽く補足をしてからwxWidgetsでのポリモーフィズムについて、もう少し詳しく踏み込んでみます。

スポンサーリンク

補足:ポリモーフィズムを身近な例で説明してみる

ここまで説明してきたポリモーフィズムですが、さらに分かりやすくするため、身近な例に喩えて説明してみます。もうポリモーフィズムの説明はいいよって方は次の節まで飛んでいただければと思います。

皆さんはコンセントにプラグを差し込んだことがあると思います。挿し込もうとするプラグは、目的によって違ったプラグだったと思います。

プリンタに電源を供給したい場合は、コンセントに挿し込むプラグはプリンタの電源プラグになっていたでしょうし、冷蔵庫の場合は冷蔵庫の電源プラグになっていたことでしょう。

コンセントとプラグの部分は同じでも、その時々によってプラグの先にあるものは変わっていたということです。

ですが、皆さんはプラグの使い方、つまり、挿し込み方を気にしたことはなかったはずです。どの製品の電源プラグであっても、「それが電源プラグである」と分かりさえすれば、迷いなくコンセントに差し込むという使い方をしていたはずです。

例えば「プリンタの場合は、プラグを逆向きに挿し込んで、冷蔵庫の場合はプラグをそのまま挿し込んで・・・」といったようなことを考えたことはないはずです。

使い方が統一されているわけですね。ですが、プラグを挿し込んだ結果は、具体的にどの家電製品につながっているプラグだったのかによって変わってきます。

プリンタについていたプラグならプリンタが動きますし、冷蔵庫に付いていたプラグなら冷蔵庫が動きますよね。

プラグを使う側は、具体的に利用したい製品を指定しさえすれば(プラグを選びさえすれば)、その後は共通化された使い方(プラグをコンセントに差し込む)で、違った結果(プリンタを動かす、冷蔵庫を動かす)を起こすことができています。

ポリモーフィズムもこれと同じで、具体的に利用したいクラスを指定しさえすれば(インスタンスを作成しさえすれば)、その後は共通化された使い方(ポインタを経由して関数を呼び出す)で、違った振る舞い(前回記事の実行結果を得る、今回の実行結果を得る)を起こすことができます。

このように理解すると、多少はポリモーフィズムも分かりやすくなるのではないでしょうか。

ポリモーフィズムの例は、他にも「フライパンとガスコンロ」「住所と配送」などなど、色々と見つかりますから、気が向いたら探してみてはいかがでしょうか。

ポリモーフィズムの利用方法

ここまで説明してきたポリモーフィズムですが、今回のソースコードではMainFrameクラスにwxFrameクラスを継承させることで実現していました。

このwxFrameクラスはウィンドウを管理するためのクラスだということは前回記事で解説しました。

そのwxFrameを継承したわけですから、MainFrameクラスには、何もしなくてもすでにウィンドウを管理するための機能が一通り揃っているということになります(この説明が分からない方は、クラスの継承について勉強してください。ですが、この記事を読むだけなら、ざっくりと「子クラスは親クラスの機能を使える」と考えておいていただければ問題ありません)。

ですから、後はウィンドウ上にコントロールを配置する方法を理解できれば、好きなGUI画面を作れるようになります。

ということで、この節でコントロールの配置方法を解説します。とは言っても、そう難しいことではなく、ウィンドウ管理のクラス(今回はMainFrame)のコンストラクタ上でコントロールのインスタンスを作るだけです。

具体的にはMainFrameコンストラクタの次の部分が相当します。

...(略)
{
  /* メインウィンドウに必要な要素を作る */
  // メニューバーのファイル項目に配置する選択肢を作る。
  wxMenu* file_items = new wxMenu();   // 作った選択肢を管理するためのインスタンスを作成
  file_items -> Append( static_cast<unsigned int>(EventID::FILE_MENU_QUIT), wxT("&Quit"), wxT("Quit this application") ); // Quit という選択肢を作成

  // メニューバーを作る
  wxMenuBar* menu_bar = new wxMenuBar();  // メニューバーを管理するためのインスタンスを作成
  menu_bar -> Append( file_items, wxT("&File") ); // File というメニュー項目を作成

  SetMenuBar( menu_bar );   // このウィンドウ上のメニューバーとして、 menu_bar を表示するように伝える

...(略)
}

今回はメニューバーだけを配置しています。

ウィンドウ上にコントロールを配置しているようなイメージを持てば理解しやすいかと思います。そして、何を配置するかは、ここではコンストラクタで指定していると。

MainFrameはOnInit()関数でshow()関数を呼び出さないと画面上に表示されませんでした。

ですが、wxMenuはSetMenuBar()関数を呼び出しておくと、MainFrameのメンバ関数show()が呼び出されたときに、一緒に表示されるようです。

公式リファレンスのSetMenuBarの項目を読んでみると、「フレームにメニューバーを表示するように指示を出す」といった内容が記載されていました。なので、show()を呼び出したタイミングでなくても、好きなタイミングでメニューバーを表示できそうです。

まとめ

想像以上にポリモーフィズム解説が長くなったので、本来ならこの記事で説明したいことを説明できなくなってしまいました。

ということで、今回の記事のまとめです。

  • ポリモーフィズムとは、動的に(プログラムの実行中に)処理を変える仕組みのこと
  • ポリモーフィズムはプログラム内の視点を考えながら理解すると分かりやすいかもしれない
  • wxMenuクラスのインスタンスはSetMenuBarを呼び出して配置する(show()関数を呼び出す必要はない)

そのため、前回記事のやり残し(wxWidgetsを利用する上で知っておかなければいけないこと7選の6番と7番)はまたまた次回に回すことにします。

前回記事のやり残しの解説を楽しみにしていてくださった方がいらっしゃったらすみません。ですが、ポリモーフィズムの考え方もwxWidgetsを利用する上で必須の考え方になるので、押さえておいていただければと思います。

タイトルとURLをコピーしました