Visual C++ の resource.h を考え直す

何故今頃になって、*.rc やら resource.h やらの話になるかというと(誰も分からない/困らないというのはさておき)、

.NET におけるアプリケーション アーキテクチャ ガイダンス
http://www.microsoft.com/ja-jp/dev/2010/solutions/architecture/default.aspx

をざっと読み返していて、かつてのVB2.0やらVC++5(あたり?)から基本的な「レイヤー」という概念は変わっていない、というところを考えていたわけです。当時(主に15年前)は、メモリーの制約が大きかったり、回線が細すぎたり(それでもパソコン通信の9600bpsに比べれば、社内LANの10Baseは天国だった訳で)する「制約」があり、このためにもメモリ効率やCPUのリソースを効率的かつ直接的に扱う C/C++ が好まれたわけです。VM という意味では、Pcode を使った VB2.0 が既に出ているので「実運用」に耐えることができた(当時、Excel VBAで業務アプリケーションを組んでいたし)、ちょうどそんな狭間の時期ですよね。いやいや、既に smalltalk や lisp があったので、スクリプト言語という概念も普通にあったわけで。emacs を使っていたし、もっと進んでいたハズ。

オブジェクト指向の概念はまだ「パターン」という名前に発展はしていませんでしたが、既にありました。が、好意的に言えば「まだ、こなれていなかった」あるいは「現実解を選んだ」のが、Microsoftが生み出したMFCというクラスライブラリ群と *.rc を中心とするデザイナです。wysiwyg という奴ですね。

ディスプレイ上でマウスを使いながらぽちぽちと部品(ボタンコントロール)などを配置します。今となっては当然の作りになっていますが(いや、Qtとかtcl/tkとか使うと座標指定している?)、VB2.0 が出た頃は「これは便利」と皆さま相当使ったものです。
さて、VB で好評を博したこのインターフェースを、VC に持ってきたのが Visual Studio のリソースエディタなのですが、今から考えれば「コントロールは名前で制御すればよかったッ!!!」って具合でしょうか?

VB の場合、画面リソースは、

VERSION 5.00
Object = "{648A5603-2C6E-101B-82B6-000000000014}#1.1#0"; "MSCOMM32.OCX"
Begin VB.Form Form2
   Caption         =   "Terminal"
   ClientHeight    =   3450
   ClientLeft      =   60
   ClientTop       =   345
   ClientWidth     =   3600
   LinkTopic       =   "Form2"
   ScaleHeight     =   3450
   ScaleWidth      =   3600
   StartUpPosition =   3  'Windows の既定値
   Begin VB.PictureBox Pic_Info
      Height          =   255
      Left            =   3120
      ScaleHeight     =   195
      ScaleWidth      =   315
      TabIndex        =   9
      Top             =   3120
      Width           =   375
   End
   Begin VB.CommandButton IDB_DISCONNECT
      Caption         =   "Disconnect"
      Height          =   255
      Left            =   120
      TabIndex        =   8
      Top             =   2760
      Width           =   975
   End
...
End

のように、「Pic_Info」や「IDB_DISCONNECT」という名前(文字列)で管理しています。この名前の namespace は、このフォームの中で有効です。なので、他のフォームを作ったときにも、同じ「Pic_Info」や「IDB_DISCONNECT」という名前を付けられるのです。これらの区別は、Form1.Pic_Info と Form2.Pic_Info とは別のものとして扱われます。当たり前といえば当たり前のような気がするのですが…

VC++ の場合はどうでしょうか? *.rc のダイアログリソースを除いてみると。

IDD_HC_WAVE_VRETICAL_PAGE DIALOGEX 0, 0, 279, 193
STYLE DS_SETFONT | WS_CHILD | WS_DISABLED | WS_CAPTION
CAPTION "Vertical"
FONT 8, "MS Sans Serif", 0, 0, 0x0
BEGIN
    LTEXT           "Group name:",IDC_STATIC,5,10,77,8
    LTEXT           "Hatch cover no:",IDC_STATIC,5,26,77,8
    LTEXT           "Length of hatch opening (m):",IDC_STATIC,5,42,101,8
    LTEXT           "Breadth of hatch opening (m):",IDC_STATIC,5,57,101,8
    LTEXT           "Hatch center location FP (m):",IDC_STATIC,5,74,101,8
    LTEXT           "Pressure P (kN/m2):",IDC_STATIC,5,91,101,8
    EDITTEXT        IDC_E_HC_WAVE_LOADS_GROUP_NAME,114,10,92,12,ES_AUTOHSCROLL
    EDITTEXT        IDC_E_HC_WAVE_LOADS_BREADTH,114,57,92,12,ES_AUTOHSCROLL
    EDITTEXT        IDC_E_HC_WAVE_LOADS_HATCH_COVER_NO,114,26,92,12,ES_AUTOHSCROLL
    EDITTEXT        IDC_E_HC_WAVE_LOADS_LENGTH,114,42,92,12,ES_AUTOHSCROLL
    EDITTEXT        IDC_E_HC_WAVE_LOADS_CENTER,114,73,92,12,ES_AUTOHSCROLL
    EDITTEXT        IDC_E_HC_WAVE_LOADS_PRESSURE,114,89,92,12,ES_AUTOHSCROLL
END

「IDC_E_HC_WAVE_LOADS_GROUP_NAME」や「IDC_E_HC_WAVE_LOADS_BREADTH」は一見名前のように見えますが、別途 resource.h で定義されている数値なのです。

#define IDC_E_HC_WAVE_LOADS_GROUP_NAME 1000
#define IDC_E_HC_WAVE_LOADS_BREADTH    1001

で、この数値を使って VC++ の場合は、テキストボックスの CEdit を扱います。

CEdit *editName = (CEdit*)this->GetDlgItem(IDC_E_HC_WAVE_LOADS_GROUP_NAME);

あるいは

CEdit m_editName;
...
DDX_Control( ex, IDC_E_HC_WAVE_LOADS_GROUP_NAME, editName );

まあ、これはこれで良いのですよ。文字列のようにメモリを喰う(あるいは定義しにくい値)を使うよりは、数値で受け渡しができると高速化できるし。実際、私も当時は、GetDlgItem を使って CButton や CEdit にキャストをしていました。
が、問題はこの数値の定義が、resource.h に集まってしまう(あるいは何処かで define しなければいけない)というのがあります。

VB の場合は、フォーム毎に「Pic_Info」という文字列を定義しているし、コントロールを特定するにしても「Pic_Info」という文字列と比較するので、他のファイルなどを参照する必要はありません。勿論「Pic_Info」という文字列を「Picutre_Info」のようにタイプミスしてしまう問題もありますが、VB の場合は、コントロールの名前=コントロール自身を示す実装となっているため、このタイピングミスが発生しません。

ですが、VC++ の場合、一見 VB のデザイナを継承した作りになっていますが、resource.h に一度定義した値を *.rc の値とマッピングさせているために、二度手間が発生しています。さらに、namespace の概念は、resource.h 全体に広がってしまうので、アプリケーションで使う全フォームで別々の数値を割り振る必要があります。更に云えば、C++ では #define で数値を定義することが多く、resource.h に定義されているものは、画面以外のリソース ID も含んでいるのです。実に混乱の元となっています。

コンパイル時の弊害もあって、resource.h に書かれている定義を数々のフォームで参照するために、何処かの画面を変更するたびに、resource.h が書き換えられて、他の「変更していない画面」もコンパイルされてしまうことです。これは、C++ のコンパイル時の構造上の欠陥でもあるのですが、実はフォームごとに resource.h を用意する(あるいは、フォームのヘッダファイルにコントロールIDを直接定義する)ということで回避できた要素なので、ええ「仕様ミス」ですね。
当時としては、十数画面が普通だったので(そうしないとメモリが足りなくなった)問題がないのですが、今となって百画面あっても不思議ではないので(そういう作り方をするのも問題ですが)、結構コンパイルに時間がかかります。ひょっとすると、当時の VC++ よりもコンパイルは遅くなっているかもいしれません。
(当時、MSC++ はコンパイルが遅いので有名だったので、正確なところはわかりませんが。そうそう、toubo c++ が早かった。gcc は遅かった)。

では、XAML の場合はどうなっているかというと、コントロールの識別は文字列(Name属性)として扱います。また、デザイナ&コンパイラは、VB と同じように、文字列とコントロール名を自動的に相互変換してくれます。
ちなみに、Xcode + objective-c の場合は、Outlet という形で手動でデザイナとマッピングさせています。

一応、当時の VC++ を擁護すると、それぞれのコントロールは window handle に結び付けられているので、これを作るときに int 型の値を使ったほうが効率が良かったのですよ。metro style をはじめとする silverlight や WPF などの XAML の場合には window handle を持たずに、直接 XAML の構造から検知をします。なので、この構造の違い(window class struct の違い)が、ここの「違い」になっています。
とはいえ、VB の場合は、名前で相互変換してるわけだから、VC++ の場合もそれを踏襲すればよかったのに…愚痴愚痴。という具合です。

なので、MFC 由来の CButton や CEdit という標準コントロールクラスと、XAML で提供されるコントロール(System.UI)とは相性が悪い、ってことなんですかねぇ。ひたすら、VC++ が XAML を扱えないのは、MFC 自体がネックになっているのかもしれません。
そういう意味では、c++/cx から MFC をひっぺはがして、WinRT という COM で再構築をして、便利クラス部分(Platform::String関係)とUI部分(Windows::UI::Xaml関係)だけを残すというのは利に叶っているような気がします。

で、

まぁ、metro style の場合は必要であれば c++/cx で組む(OpenCV, Fortran の組み合わせとか)選択肢もあるのですが、じゃあ desktop はどうするのか?ってことで、c++/cli を使い続けるのか(インテリセンスが効くようになったし)、画面部分は C# に任せて、コアなところは MFC を含むVC++ で組むのか、ということになります。更に、既存のアプリに新しい画面を追加するのは、この resource.h と付き合っていかなければならないのか?ってことになります。まぁ。レガシーといえばレガシーなわけですが。

XAML 時代のアイデアを借用して、デザイナ自体を C# で作り、そして C++ のコードを吐き出させるという逆向きのやり方もありかな、と思っています。C++ だけで、画面を wisywig で作成した後に、*.rc, resource.h かつ、フォーム関連のヘッダファイルを整合性があうように弄る、というのは結構な手間なのですが、これを C# でやろうとすれば効率よく書けるでしょう。特にデザイナ部分は楽かも、と思ってしまいます。

ざっとアイデアを書いておくと、

方針としては、VB, XAML のように「文字列」で各コントロールの名前付けをする、ってことです。そうして、フォーム単位で namespace が有効になるような構成に仕立て直すのです。
既存の CButton, CEdit はそのまま流用できたほうがよいので、これを拡張できるようにしていきます。

class Form1
{
public:
	static int IDD				    = (int)"Form1";
	static int IDC_GROUP_NAME 		= (int)"GroupName";
	static int IDC_LOADS_BREADTH    = (int)"Breadth";
protected:
	CEdit GroupName(IDC_GROUP_NAME);
	CEdit Breadth(IDC_LOADS_BREADTH);
...
};

C++ の場合、値の初期化が直接できないので、考え直す必要がありますが、上記のように文字列をint型に直しておきます。クラス内で定義するので、定義自体は、Form1::IDC_GROUP_NAME としてアクセスします。ここの文字列と、コントロールの名前は「CEdit(“GroupName”)」のように一致させます。ここでは、CEdit(IDC_GROUP_NAME) のようにして値を設定しています。

リソースエディタのほうは、以下のように namespace を指定するか、

Form1::IDD DIALOGEX 0, 0, 279, 193
STYLE DS_SETFONT | WS_CHILD | WS_DISABLED | WS_CAPTION
CAPTION "Vertical"
FONT 8, "MS Sans Serif", 0, 0, 0x0
BEGIN
    LTEXT           "Group name:",IDC_STATIC,5,10,77,8
    LTEXT           "Hatch cover no:",IDC_STATIC,5,26,77,8
    EDITTEXT        Form1::IDC_GROUP_NAME,114,10,92,12,ES_AUTOHSCROLL
    EDITTEXT        Form1::Breadth,114,57,92,12,ES_AUTOHSCROLL
END

次のように、直接文字列を指定してマッチングを取ります。

"Form1" DIALOGEX 0, 0, 279, 193
STYLE DS_SETFONT | WS_CHILD | WS_DISABLED | WS_CAPTION
CAPTION "Vertical"
FONT 8, "MS Sans Serif", 0, 0, 0x0
BEGIN
    LTEXT           "Group name:",IDC_STATIC,5,10,77,8
    LTEXT           "Hatch cover no:",IDC_STATIC,5,26,77,8
    EDITTEXT        "GroupName",114,10,92,12,ES_AUTOHSCROLL
    EDITTEXT        "Breadth",114,57,92,12,ES_AUTOHSCROLL
END

ただし、リソースの場合は、リソースコンパイラが動くのでファイル構造は変えられないので、上記の場合は適当なプリプロセッサを通して名前を変換するのがよいかと。

一度 resource.h に直すのもアリなのですが、依存関係が Form1 <- resource.h になりそうなので、パスですかねぇ。
C/C++ の場合、同じ文字列は同じアドレスに格納されるので、コントロールの名前は strcmp をせずに int 型の比較(正確に言えばポインタの直接比較)で同定ができます。

まあ、実際作るかどうかは別として、レガシー MFC/C++ のアプリの改修に、resource.h のコンパイル地獄で時間を取られるのは嫌なので、コントロールを文字列で同定できるハッキングはしたいなぁ、と考えています。つーか、やらないと駄目かも。

カテゴリー: C++ パーマリンク