[今までのあらすじ]

本サイトは、2002年にリリースしたWin32 APIベースのプログラミングを支援する(Win32)リソースエディターであるBCCForm、リソースファイル(*.rc)からスケルトンを作成ツール(SkeltonWizard)とその利用のためのライブラリーであるBCCSkeltonの利用をサポートするために2021年にリオープンしました。

 

リオープン後、20年前の旧ボーランドC++コンパイラーであるBCC55.1(bcc32.exe)から、承継会社、EmbarcaderoのClang C++コンパイラーであるBCC102(bcc32c.exe)へ移行し、BCCSkeltonを使って開発した様々なアプリをサンプルとして同梱し、更に追加クラスやUnicode対応の互換ライブラリー(ECCSkelton)もリリースしました。

 

その後、個人が使用するアプリを検討している中でWEB上の記事から「WIndows 10、11のユーザーはC#とVBのコンパイラーが使える」ということが分かり、その利用のために本年MSCompAss(Microsoft Compiler Assistant)を開発、そのバージョンも1.4迄となり、未だC#の学習にはまっているというのが現状です。

 

しかし、ネックとなるのが(あの巨大且つ自動化された)Visual Studioを使わずにC#プログラム(注)を開発しているので、Visual StudioのIDEで提供されているVisual デザイナー、リソース・XMLリソースの生成・管理ツールが全くないことです。

 

ということで、20年前に「リソースエディターが無いんなら、作っちゃおう」というBCCForm開発のスピリットで、C#専用リソースファイル(*.resourcesと*.resxファイル)作成ツール(ResWriter)とリソースファイル確認ツール(ResReader)を学習(OJT)の一環として開発してきました。

 

[初版からの発展]

どうもダイアログやメニュー等(C#でjは本体のコードで生成する)を含むWin32リソースとは全く違いますし、バイナリーベースの*.resources(埋め込み用)とXML言語ベースの*.resxファイルがあり、色々と壁に頭をぶっつけながら「C#の大きなリソースファイルは『埋め込み(*.resources)』と『外部参照(*.resx)』で、大きな区分は『イメージ(画像)』『文字列』『ファイル』」という括りでやってきました。『埋め込み(*.resources)』と『外部参照(*.resx)』は、リソース周りのクラスが充実しているので非常に助かりましたが、次の問題点が残りました。

 

(1)『イメージ(画像)』リソースも、GDI+ベースのImageクラスを使って一本化したのですが、これをリソースに追加(AddResourceメソッド)すると、すべてBitmap形式になってしまうこと。

(2)『文字列』リソースは一つ一つを手入力することで対応できるのですが、特殊なテキストファイルから一括入力ができない。(注)

注:ResWriterの『ファイル』リソースは、ファイルをbyte[]ストリームで書き込んで作成するようになっていますが、 Microsoft の ResGen.exe ツールだと「key=value」という書式を使って"*.restext"ファイル、"*.txt"ファイルから文字列リソースを取り込めむことができます。

 

(1)『イメージ(画像)』リソース

Bitmapリソースは今まで通りとし、とつぃ扱いがやや特殊なIconリソースはIconクラスを使ってストリーム出力し、その他の画像データ形式はImageクラスのImageFormatを"RawFormat"にしてストリーム出力するようにして問題を解決しました。

(2)『文字列』リソース

『ファイル』リソースを作る際に(C#のリソース専用ファイル拡張子と考えられる)"*.restext"ファイルは問答無用で、また"*.txt"はダイアログで「文字列リソースに取り込みますか?」と確認して ResGenと同じように「key=value」形式で文字列リソースをファイルから取り込むことができるようにしました。(以下はMicrosoft Docsの例にあったPatients.txtファイルを文字列として読み込みました。)

 

ということで、あとはResReaderを完成させれば一応の完結(「猫でも」も残りわずかになりましたし)にしたいと考えています。

 

C#プログラミングの習作として作成したResWriterでは、bitmap、icon、jpeg等その他イメージをImageクラスで簡単に読み取れるので十把一絡げで"Image"にまとめてしまいましたが、ResourceWrite、ResXResourceWriterrクラスで保存すると皆bitmapになってしまいます。これはやはり習作の(ResGeneを転用した)ResReaderで見ると明らか(注)です。

        //Imageリソースの書き込み(現行コード)
        Image img = Image.FromFile(item.SubItems[2].Text);    //ListViewのファイル名でImage Class objectを作る
        res_writer.AddResource(item.SubItems[0].Text, img);     //ResourceWriterでリソースを追加(*.resourcesファイル)
        resx_writer.AddResource(item.SubItems[0].Text, img);   //ResourceWriterでリソースを追加(*.resxファイル)

注:最初のicoCSはiconファイルで、次のimgCatはjpegファイルです。これはWindowsの内部処理では画像データの規定値をビットマップにして扱っている為なんじゃなかろうかと推定しています。

 

これではあんまり雑なので(おそらくはGDI+をベースにしていると思う)Imageクラスに何かデータ形式にかかわる内部情報を持っているのではないか?、と考え調べてみました。その結果、画像データ形式はImageFormatクラス(値の例:ImageFormat.Bmp、ImageFormat.Icon等)があり、識別できること、およびリソース書き込みの際に AddResource メソッドにオーバーロードがあり、↑のコードのように直接Imageオブジェクトを書き込む他に、(データの)ストリームで書き込むことができることがわかりました。それを使えば↓のようにオリジナルのデータでリソースを書き込めます。

        MemoryStream imgStream = new MemoryStream();    //Image streamの作成
        Image img = new Image(img_filename);    //ファイル名(img_filename)からImageオブジェクトを生成
        img.Save(imgStream, ImageFormat.(value));    //value形式でImageオブジェクトをメモリーに書き込む

        ResourceWriterオブジェクト.AddResource(データ名, imgStream);

因みに前に作った(猫の画像の)jpegデータを書き込んだリソースファイルがあったので読み込むと、

↑のように「PinnedBUfferMemoryStream」という表記になりました。

 

ResWriter、ResReader共にイメージリソースの区分を細分化してストリームで書き込むか検討します。(注)

注:ストリーム(queを使ったデータ処理)を使う場合、(当たり前ですが)使っている間はImageオブジェクトをdisposeできません。従って事故的に止まるような例外処理対応(try-catch-finallyまたはusing())を必ずやれ、というお達しです。

 

ps. ところで余談ですが、Microsoft Learningの中に「埋め込みリソースは*.recourcesファイル」を前提とする記述がありましたので、前回の私の考え「(*.resources-*.resxも埋め込めますが、テキストファイルを埋め込むよりはバイナリーの方がサイズが小さいので実質的に*.resources一本やりでよいのでは?)」でよかったようです。

 

ps of ps. しかし、大分はまってしまい、BCC(ECC)SkeltonそっちのけでMSCompAssをいじってます。本当にC++のWin32 APIプログラムを書けなくなっちゃうのではないかと心配です。(newしっぱなしで、delete忘れそう。)

 

ふー、ちょっと疲れました。

たった今、試行錯誤の末、簡単なリソースライター、「ResWriter」の初版を脱稿した所です。

 

えっ?ResGeneというリソースリーダー・ライターを作っていたのじゃないのか?、とね?

 

その通りなんですが、どうも「リソースファイルを読み書きするツール」という概念が腑に落ちなくて、悩んでいたこと、ご高承のとおりです。(「【C#学習】雑談-用語と概念整理、Resources周り」)で、結局「リソースファイルを読んで、また書くツールは、C#のリソースが単にイメージ、文字列、ファイルだけなのであまり意味がなく、『リソースを読んで、リソースファイルを書く』ことと、『リソースファイルを読んで、リソース(の内容)を書く』しか意味がないのではないか」と思い当たりました。

 

と、いうことで...

 

すぐに方針変換して、まずは「リソースを読み込んで、リソースファイル(*.resources、*.resx)に書き出すツール」を作ってみようということで「ResWriter」に着手しました。

 

「仕様」はリソース区分を次のようにして、入出力を「私的に統一」してみました。

1.イメージファイル(区分 Image)→Imageオブジェクトで読み込み、書き出す(Bitmap変換されるようです。)

2.ファイル(区分 File)→ファイルを読み込んでバイトデータにシリアライズし、ストリームで書き出す。

3.文字列(区分 String)→手書き入力して書き出す。

 

これがその外観。

 

入力ダイアログ。こいつはリソース区分により「ファイルを開くダイアログボタン」や「入力エディットボックス」が、ラジオボタンの横に現れたり、消えたりします。

 

 

ドラッグアンドドロップもでき、その際にも使う編集ダイアログ。(データを編集できるのは区分Stringだけです。)

 

削除と編集は複数リソースをまとめて処理できる複数選択が可能です。

 

出力は(現在の段階では、その内容をこれから読んでチェックしてゆかなければならないので)一度に*.resourcesファイルと*.resxファイルの二種を作成します。(本日の段階で上の4つのリソースについては両ファイルともにちゃんと出力しているようです。)

 

次のステップは、このResWriterをブラッシュアップするために、先のResGeneのスケルトンを改造してResReaderを作る予定です。(といっても大したことはせず、リソースファイルに書かれている内容をResourceReaderのEnumeration機能により列挙するだけです。(↓のようなResourceReaderの出力(「リソース名: リソースデータ(Type リソースタイプ)」)になる予定です。)

 

ps. マジでC#プログラミングを始めちゃって「猫でも」学習をおろそかにしている今日この頃、でした。

 

先般Anchorの話を書きましたが、C#のコントロールにはDockプロパティというものがあり、下は順に「上」「下」「クライアントエリア」「右」「左」の端にコントロールを位置づけ、サイズ変更時にも自動サイズ変更するというものです。

 

        Dock = DockStyle.Top;
        Dock = DockStyle.Bottom;
        Dock = DockStyle.Fill;
        Dock = DockStyle.Right;
        Dock = DockStyle.Left;

 

BCCSkeltonでWM_SIZE(OnSize関数)を捉えて、親のクライアントエリアの差分、コントロールのウィンドウサイズの修正をしてAnchor機能を出していましたが、Dockについても同様でした。(詳しいCodeはAnchorの話参照)

そういう環境からみると、いやぁ、至れり尽くせり、という感がありますが、一方、上と上はもとより、例えば上と左にDOckさせた場合の「どちらがクライアントエリアの全高・全幅をとるか」という優先順位やコントロール同士のオーバーラップ管理はVisual StudioのGUIデザイナーが担当しているので、ユーザーによる管理はちょっと面倒だそうです。

まぁ、「猫でも」で初めてDockのことを知った際には「便利だな」と思いましたが、複数コントロールの位置管理の問題があれば、DockではなくAnchorを使って調整するのがよいように感じます。

 

すでに何度か「C++とC#は基本的に別物で、C(++)文法で書かれる(Garvage collection-以下GC-のある高級言語の)Visual Basicみたい」と書きましたが、益々その感を強めています。

 

今までC#を学習してきて引っ掛かりを覚えた所とかをランダムに上げていくと...

 

1.Stream、Serialize and que

リソース関係で中核となるResourceManager、ResourceSet、ResourceWriter、ResourceReader辺りをMicrosoft Docで眺め、"GetObject"()と"GetStream"が目に付きます。(一度GetObjectでbitmapを読もうとして読めず、Streamにして読んだら読めたから印象が深い。)

)C#で「リソース」という場合、C++プログラマーがすぐに思い浮かぶ「(正にBCCFormで扱ってきた、メニュー、ダイアログやコントロール等の)Win32リソース」の概念とは全く違うことを肝に銘じる必要があります。あるサイトではC#の「リソースの種類」として「文字列、イメージ、アイコン、オーディオ、ファイル(text、binary)その他」に区分していましたが、(直球ど真ん中で「リソースの種類」で言及はしていないものの)Microsoftは「アプリ リソースとリソース管理システム」の中で「...

アプリの文字列、画像、ファイル リソース...アプリ リソースには、次の 2 種類があります。
ファイル リソースは、...埋め込みリソースは、」と書いております。

Microsoftは昔からMFCの中で"Serialize"をよく使っており、「様々な複雑なデータも順に一直線のデータにすること()」だと理解しており、その理由(必要性)は移動、複写、参照等データ処理の為です。

:「シリアライズとは、複数の要素を一列に並べる操作や処理のこと。単にシリアライズといった場合には、プログラムの実行状態や複雑なデータ構造などを一つの文字列やバイト列で表現する「直列化」を指すことが多い。」(IT用語辞典)

 

で、データ処理となると"FILO(First-In、Last-Out)"(スタック、stackですね)や"FIFO(First-In、Last-Out)"(キュー、queですね)が思い浮かびますが、将にStreamとは大量のデータを待ち行列(que)に均して直線化すること、されたもの、と理解しており、まぁ、間違いはないでしょう。

 

2.Object、Field、Property、Method

別に碌に詳しくないOOP(「オブジェクト指向プログラミング(Object Oriented Programing)」)の講釈をたれようとは思いませんが、C++をやっているとObjectが「設計図」としてのクラス(それを「実体」化させるとインスタンス)、そのObjectが持つProertyがメンバー変数等(Propertyは財産、資産の意味だけど、resources-資源の概念とは違います)、そのObjectがする仕事(動作)のMethodがメンバー関数として覚えていましたので、Propertyが「Field(データベース用語ですね-C++ではメンバー変数)にアクセス(get;、set;)するクラス」であることを知り、感慨にふけったものです。

 

3.CLR、CIL、CLI、CTS、CLS

このうちいくつ分かりますか?(「なんのこっちゃ?」と感じるのが普通の人です。)

これらはC#とは直接関係がありませんが、その背景となる現在のMicrosoft(およびWindows)の「根っこ」と「構造」を理解する為に必要ですね。私の理解だと↓のようになります。

 

共通言語ランタイム(CLR-Common Language Runtime)→やはりC#とVBは同じ実行基盤(例外処理、ガーベジコレクション、スレッド管理などを共通とするDLL)で動作していたようです。これがC#やVBの共通データ型仕様(CTS-Common Type System/共通型システム)や共通中間言語(CIL:Common Intermediate Language→異なるプログラミング言語に共通の仕様(CLS-Common Language Specification/共通言語仕様)に基づく)で書かれたプログラムを、起動時にその環境(OS)用にコンパイルして実行するようです。

CLI (Common Language Infrastructure/共通言語基盤)→共通データ型仕様(CTS-Common Type System/共通型システム)、共通プログラミング言語仕様(CLS-Common Language Specification/共通言語仕様)に基づくそうです。

 

これらによって".NETは、"異なるプログラミング言語のみならず、異なるハードウェアやOSを超えてプログラムを共有することが可能になるわけです。(出典

 

4.Assembly

C++だとCompile、Linkして生成する「(実行)プログラム」といいそうな、データと実行コードの集合体をC#ではこう呼ぶような。(例:GetExecutingAssembly()でEXEファイルかDLLファイルかを識別する、等)じゃぁ、「Assemblyを作り出すのはAssemblerじゃないの?」と突っ込みたくなるけど、それは大昔ニーモニック(mnemonic)を機械語にするアッセンブラーがあったのでいまだにコンパイラー、リンカーと呼んでいますね。あー、混乱する。

 

5.(余談)CloseとDispose

CやC++というアッセンブラー的にメモリー管理をプログラマーがやらなくてはならない低級言語を経験せずに、C#という高級言語から入った方が「CloseとDisposeとはどう違うのか?」で、これとかあれとか結構論争しているのを見るのも新鮮です。この論争の原因は、以下のような状況の為かもしれません。

・CやC++の言語仕様に則ったC#ですが、GCがあるので(メモリーリークという恐怖を背景にした)「newしたら必ずdelete」「openしたらなからずclose」「createしたら必ずdelete」等の「お片づけ」作法をしつけられていない"spoiled children"という印象を受けます。

・GCもすべてMicrosoftが完璧にやってくれるのならよいのですが、実行基盤であるCLR(↑参照)が管理するプログラム資源(リソース)は危なくなったらGCをかけて甘やかす一方、ユーザー管理にゆだねなければならない「ファイル」「リソース」関係は「自分でお片づけしなさいっ!」と突き放すので、「Managed resources、Unmanaged Resourcesってなに?(要すれば、CLRが自動的にGCしてくれるリソース、そうでないリソース)」と混乱し、Win32 APIをいじったことがない人には「CloseとDisposeってどう違うの?」ということになります。(Open-Closeはデータへのアクセス可否、Dispose(廃棄)はメモリーを含むプログラミング資源の開放<delete-削除、release-解放、destroy-破壊等の用語がWin32 APIでも混在していますが...>)

・その混乱を助長するようにMicrosoftが提供するメソッドでCloseの中にDisposeがj入っていたり、Disposeの中にCloseが入っていたりすることもあるようです。

私なんかですと「メモリーリークしないように、コンピューターで遊んだ後は、ちゃんとお片づけすればよいだけ」と感じますが、どうでしょう?

 

閑話休題。

 

MSCompAssに続く、C#用の外部リソース(*.resx、*.resources)、埋め込みリソース(*.resources-*.resxも埋め込めますが、テキストファイルを埋め込むよりはバイナリーの方がサイズが小さいので実質的に*.resources一本やりでよいのでは?)を作成するツールのスケルトンは完成していますが、未だ入出力管理仕様が決まっていません。

 

その理由は以下の通り。

(1)リソースの種類は、実質GetStringかGetObjectしかなく(ローランド風に)「俺(string)か、俺以外」しかない現状です。(注)

注:Win32リソースのようにDIALOG、MENU,BITMAP、ICON等の区分はなく、基本「一本化」されていますが、文字列(string)だけはUnicodeなので「カルチャ(注の注)」という要素があり、ローランドになったように思えます。

注の注:「カルチャ」とは「軽茶」ではなく、Cultureのこと(理科系の人は「ー」を使いません!)で、C++では"locale"といってました。これも↑のネタにしたかったですね。

(2)したがって、テキストファイル、(画像を含む)バイナリーファイル等ファイル系は皆シリアライズしてストリームにしてもよいのですが、それを受け入れるときにBitmap、Icon、Byte等「異なる器を用意する」必要があるので「読む」際にどうするか?

(3)「書く」際は皆AddResourceでよいのですが、その対象をGetString、GetObject、FromStreamの区分があり得るので、これはどう判断すべきか?

(4)そもそもリソース周りはResourceManager、ResourceSet、ResouceReader、ResourceWritermの4役で仕切ることでよいと思うのですが、これらが必ずしも排他的な関係ではないので、相互に役割がどう関係づけられるのか、を整理しないといけないのではないか?と考えています。

 

まぁ、これができちゃうともうやることがなくなるので楽しい「おもちゃ」としてゆっくりと作りましょう。

 

本日のブログで紹介しているCTTIPクラスのテストプログラム(ToolTip.exe-注)が、(ToolTipProc.hで)最初にしている処理は、例のTOOLINFOW構造体のUnicodeのサイズ問題の際にコモンコントロールのDLL(cmmctrl32.dll)のバージョンにより不具合が出ているのでは、ということで確認のために入れていました。ご参考までに紹介します。

注:BCCForm_and_BCCSkelton→ECCSkelton→ECCSkeltonSample→"11 ToolTip"参照)

 

    //コモンコントロールのDLL(cmmctrl32.dll)のバージョン確認(ECCSkelton版-ANSI版は緑字を修正してください
    DWORD dwVersion = 0;
①  HINSTANCE dll = LoadLibraryW(L"comctl32.dll");
    if(dll) {
②      DLLGETVERSIONPROC GetVersion = (DLLGETVERSIONPROC)GetProcAddress(dll,"DllGetVersion");
        if(GetVersion){
            DLLVERSIONINFO dvi;
            HRESULT hr;
③          memset(&dvi, 0, sizeof(dvi));

            dvi.cbSize = sizeof(dvi);
④          hr = GetVersion(&dvi);
            if(SUCCEEDED(hr)) {
                WCHAR str[512];
                wsprintfW(str, L"Version:%d.%d.%d.%d", dvi.dwMajorVersion, dvi.dwMinorVersion, dvi.dwBuildNumber);
                MessageBoxW(m_hWnd, str, L"Version of comctl32.dll", MB_OK | MB_ICONERROR);
            }
        }

      FreeLibrary(dll);
    }

"comctl32.dll"をロードし、そのインスタンスをdllに読み込みます。

②正常に読み込めたら、"comctl32.dll"の関数(または変数)名である"DllGetVersion"のアドレス(GetVersion)を求めます。

③DLLVERSIONINFOのdviをゼロで初期化します。(ZeroMemory(&dvi, sizeof(dvi));と同じ。)

④"DllGetVersion"関数を、dviのアドレスを引数に呼びます。

ロードした"comctl32.dll"を開放します。

 

因みに私のWin10、Win11マシンはいずれも、

 

Version = 5.82.10586MajorVersion, MinorVersion, BuildNumber

 

でした。なお、今回アップしたソース(ToolTipProc.h)では、間違って(as is usual with me!)".%d"が一つ余計でした。修正版は次のアップデートで載せますので、それまでは最後の".xxxxx"は無視してください。

 

いやはや、またまたタフなここ数日です。Listen, the story is this....

 

1.C#学習

最初は(C++との同一性や類似性故に)戸惑いを覚えてきた「高級言語C#の差異」ですが、C(++)文法に準えたVisual Basicと割り切って、最近は楽しくプログラミング出来てきました。

未だ「猫でも」のフォーム編をすべてカバーしておりませんが、並行して(今後絶対に必要となる)「埋め込み用リソースファイル(*.resourcesファイル)作成ツール」をまたまた自作しようと、「全て手書きとMSCompAssだけ」で開発を始めました。(要すれば、最終仕様も煮詰まっておらず、すべてが試行錯誤と学習の実習といえます。)

(注)

注:Microsoftは既にresgen.exeというプログラムを持っているので、「ロスジェネ」に準えて「ResGene」にしてみました。

 

2.MSCompAss Version 1.4

そんなこんなで、今後埋め込みリソースの為に"*.resources"ファイルを多用しなければならないのですが、csc.exeコンパイラーの"/resources:"オプションと、プログラムアイコンを指定する"/win32icon:"オプションが、現在のMSConpAssでは一緒には使えません。これを改良しなければならない、ということでMSCompAssのオプションダイアログを変更してVersion 1.4を作ったのですが、完成テストを行っていていくつか改良点が目についてしまいました。

そしてその中の致命的なものが、「ソースファイルパス名のツールチップが(複数のソースファイルを読み込むと)複数発生するという問題」でした。(注)

注:元々ToolTip(Tool Hint Control)は親ウィンドウのコントロールで兄弟のツール(コントロール)の仕様に関わる説明を行うものであり、今までは設定したツールチップのメッセージ文字列を変更する必要が無かったので単なる関数としてヘッダーファイルにして使っていました。今回も(何も考えずに)ソースファイルを読み込む度に、単純にツールチップ作成関数を呼ぶ形にしていましたが、(当たり前ですが)関数を呼ぶたびにどんどんとツールチップ(コントロール)を作ることになるので、(恥ずかしながら)ツールチップが増えてゆく(?)という異常事態になりました。

 

「BCCSkelton(およびそのUnicode版のECCSkelton)を使っていてこんな杜撰なことをしていてはだめだ。きちんとクラス化して使おう!」と思い、本日CTTIPクラスとして、先ずBCCSkelton用のSJIS(ANSI)版を書いて、それをECCSkelton用のUnicode版に落として動作試験をいたしました。そうしたら...

ツールチップが出ねえじゃんかよぅ!(泣;)

 

3.CTTIP(コードは末尾参照)

CTTIPのメソッド、じゃないメンバー関数テスト用のBCCSkeltonプログラムをSkweltonWizard→BCC2ECCでECCSkelton用にコンバートし、順調にスケルトンが動作することを確認して、CTTIPのテストコードを書き込んでゆきました。テストは、ダイアログで、以下CTTIPの関数

(1)ツールチップの付与
(2)ツールチップ文字列の更新
(3)ツールチップ最大幅設定
(4)ツールチップ背景色設定
(5)ツールチップ背景色設定
(6)フォント設定(直接指定)

のテストを行うものですが、初期的なタイプミス等のゴミ掃除を終わってコンパイルが正常に終了して、起動しても「ん?」全くツールチップが現れません。(実際、本当のバグって、エラーが発生しない場合、原因追及がとても困難になるんですよね。)

色々調べ、色々と調整したり、トラップをかけてみてもダメ。

「では」、ということで試しに今まで正常に動いていた「関数版のツールチップ」を導入したのですが、これも表示されません。

途方に暮れながらも様々なキーワードでサイトを調べまくり、とうとう、

(英語)https://social.msdn.microsoft.com/Forums/en-US/5cc9a772-5174-4180-a1ca-173dc81886d9/adding-tooltip-controls-in-unicode-fails?forum=vclanguage
(日本語)https://www.petitmonte.com/bbs/answers?question_id=12982

という関連2記事を発見。

結局、OSとcmmctrl32.dllのバージョンに関わるヘッダーファイルの定義による問題のようです。(注)

注:「ようです」というのは、私もよく分かっていないからです。ANSI版のTOOLINFOA構造体とUNICODE版のTOOLINFOW構造体は共に定義でWindows NT系のバージョン5.00以降(2000年のWindows 2000以降)については、旧TOOLINFO構造体のしっぽに"LPVOID*"(void *lpReserved;)が一つ追加されていることが原因のようです。ANSI版のsizeof(TOOLINFOA)とUnicode版のsizeof(TOOLINFOW)は共に48バイトで同じサイズであり、ANSI版では"sizeof(TOOLINFO)"で問題なく動くのですが、Unicode版ではわざわざ"sizeof(TOOLINFOW) - sizeof(void*)"として"LPVOID*"分を割愛してやらないと正常には動かないのです。(Unicode版のDLLがヘッダーファイルと不整合?)いずれにしても「くそMicrosoft、Docsの中に何か書けよ!(TOOLINFOのAとWの差を調べようと思っても、TOOLINFOWはTOOLINFOAのコピーでLPSTRをLPWSTRにも変更していません!!!)」と毒づきたくなりました。

とはいえ、この変更を行って祈るような気持ちでコンパイルして、実行ファイルをダブルクリックして...

背景色変更()、文字色変更()、テキスト変更、フォント変更(12ptのMS 明朝)と全て正しく実行されました。

 

さぁ、今晩も(慢性膵炎の疑い、なんて心配しないで)ガンガンのむぞー!

 

という気分ですね。

 

【ご参考】

///////////////////////////////////////
// ECCSkelton
// ツールチップクラスCTTIP定義ファイル
// Copyright (c) November, 2022
//        By Ysama
///////////////////////////////////////

class CTTIP
{
public:
    HWND m_hTip;                                //ツールチップのウィンドウハンドル
    HWND m_hParent;                                //親のウィンドウハンドル
    HWND m_hCtrl;                                //コントロールのウィンドウハンドル
    HINSTANCE m_hInstance;                        //親のインスタンス
    TOOLINFOW m_ToolInfo;                        //TOOLINFO構造体
    CFONT m_hFont;                                //ツールチップのフォント
public:        //メンバー関数
    bool SetToolTip(HWND, int, LPWSTR, bool);    //ツールチップの付与
    void UpdateText(LPWSTR);                    //ツールチップ文字列の更新
    void SetWidth(int);                            //ツールチップ最大幅設定
    void SetBackColor(UINT);                    //ツールチップ背景色設定
    void SetTextColor(UINT);                    //ツールチップ背景色設定
    void SetFont(int, LPWSTR);                    //フォント設定(直接指定)
};

//ツールチップの付与
bool CTTIP::SetToolTip(HWND hWnd, int CtrlID, LPWSTR pszText, bool baloon = FALSE) {

    //セットするコントロールの親、IDまたは文字列がNULLの場合エラー扱いとする
    if(!CtrlID || !(m_hParent = hWnd) || !*pszText)
        return FALSE;
    //バルーン型にするか否か
    DWORD style;
    if(baloon)
        style = WS_POPUP | TTS_ALWAYSTIP | TTS_BALLOON;
    else
        style = WS_POPUP | TTS_ALWAYSTIP;
    //対象コントロールのハンドルを取得する
    m_hCtrl = GetDlgItem(m_hParent, CtrlID);
    //文字列リソースのある親ウィンドウ(ダイアログ)のインスタンス
    m_hInstance = (HINSTANCE)GetWindowLong(m_hParent, GWL_HINSTANCE);
    //ツールチップの作成
    m_hTip = CreateWindowExW(NULL, TOOLTIPS_CLASS, NULL, style,
                CW_USEDEFAULT, CW_USEDEFAULT,
                CW_USEDEFAULT, CW_USEDEFAULT,
                m_hParent, NULL, m_hInstance, NULL);
    //ツールチップ作成失敗エラー
    if(!m_hCtrl || !m_hTip)
        return FALSE;
    //ツールチップをコントロールに関連付ける
    ZeroMemory(&m_ToolInfo, sizeof(m_ToolInfo));    //ゼロクリアー
    m_ToolInfo.cbSize = sizeof(TOOLINFOW) - sizeof(void*);
//    m_ToolInfo.cbSize = sizeof(TOOLINFOW);        //Microsoftのバグ?
//(英語)https://social.msdn.microsoft.com/Forums/en-US/5cc9a772-5174-4180-a1ca-173dc81886d9/adding-tooltip-controls-in-unicode-fails?forum=vclanguage
//(日本語)https://www.petitmonte.com/bbs/answers?question_id=12982

    m_ToolInfo.hwnd = m_hParent;
    m_ToolInfo.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
    m_ToolInfo.uId = (UINT_PTR)m_hCtrl;
    m_ToolInfo.hinst = m_hInstance;
    m_ToolInfo.lpszText = pszText;
    SendMessage(m_hTip, TTM_ADDTOOL, 0, (LPARAM)&m_ToolInfo);
    SetWindowPos(m_hTip, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
    return TRUE;
}

//ツールチップ文字列の更新
void CTTIP::UpdateText(LPWSTR pszText) {

    //TOOLINFOの準備
    ZeroMemory(&m_ToolInfo, sizeof(m_ToolInfo));    //ゼロクリアー
    m_ToolInfo.cbSize =  sizeof(TOOLINFOW) - sizeof(void*);
    m_ToolInfo.hwnd = m_hParent;
    m_ToolInfo.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
    m_ToolInfo.uId = (UINT_PTR)m_hCtrl;
    m_ToolInfo.hinst = m_hInstance;
    m_ToolInfo.lpszText = pszText;
    SendMessage(m_hTip, TTM_UPDATETIPTEXT, 0, (LPARAM)&m_ToolInfo);
}

//ツールチップ最大幅設定
void CTTIP::SetWidth(int size) {    //任意の幅を許可する場合は size == -1

    SendMessageW(m_hTip, TTM_SETMAXTIPWIDTH, 0, (LPARAM)size);
}

//ツールチップ背景色設定
void CTTIP::SetBackColor(UINT col) {    //任意の幅を許可する場合は size == -1

    SendMessageW(m_hTip, TTM_SETTIPBKCOLOR, col, 0);
}

//ツールチップ背景色設定
void CTTIP::SetTextColor(UINT col) {    //任意の幅を許可する場合は size == -1

    SendMessageW(m_hTip, TTM_SETTIPTEXTCOLOR, col, 0);
}

// フォント設定(直接指定)
void CTTIP::SetFont(int size, LPWSTR FontName) {

    m_hFont.SetFont(m_hTip, size, FontName);
}
 

C#の学習は順調です。「猫でも」のフォーム編も半ばに来ており、それとは別にコモンダイアログや主要コントロールについても独自にサンプルを作っていること、前回のとおりです。

 

所で、タイトルの"Anchor(錨)"とは「親ウィンドウのサイズ変更時(WM_SIZE)に子ウィンドウ(コントロール)をどう動かすか、を決定する固定点」のことです(よねっ?)

私が初めてこの言葉に触れたのは、Borland C++ Culider(BCB)5.0をいじくった時に、フォーム(ウィンドウ)やコントロールの「プロパティリスト」にこの言葉を見つけたときでした。最初は何のことかわからず、適当に値を選択して実際に動かして「あっ、そーなの!」という感じでしたね。(注)

注:BCBではコントロールのプロパティに"Anchors"があり、それは内訳に"akLeft | akTop | akRight | akBottom"の4つの細目をtrue | false で選択する形でした。(規定値は ”akLeft,akTop”)

 

BCCSkeltonではウィンドウクラスにこんな小技を入れるほどの余裕がなかったので、Win32 APIでWM_SIZEメッセージの際に処理するコードを書いていました。しかし、当時はこれをどう処理してよいのかがよく分かっていなかったので、コントロールの上下左右端をAnchorに設定した場合、親ウィンドウのクライアントエリアにおける位置を計算して処理していました。例えば初期のBCCMakerでは親ウィンドウ右側に置いたボタンを縦の位置やサイズは変えずに左右に動かす場合

 

(1)親のクライアントエリアをGetClientRect関数でRECT構造体に取得、コントロールのウィンドウ位置をGetWindowRect関数でRECT構造体に取得

(2)親ウィンドウのサイズ変更時に左右に動くのであれば、コントロールのRECT構造体のtopとbottomメンバーはクライアントエリア上端からの所定の距離で固定(左右の動きでは変動しますが、上下の動きでは変動しません)、leftとrightメンバーはクライアントエリアの右端からの距離を固定して再計算します。(この場合の"Anchors"に設定するのはakTopとakRightです。C#ではBCBとはちょっと異なり、コントロールのAnchorプロパティに"AnchorStyles.Top | AnchorStyles.Right"という値を設定します。→左右だけではなく、例えば右下に配置したボタン等上下にも動かしたい場合には"AnchorStyles.Bottom | AnchorStyles.Right"にします。なお、親ウィンドウと一緒にサイズを変えたければ上下左右に錨を下すことになります。)

(3)位置(サイズを変更する場合には幅 = right - left、高さ = bottom - top も再計算します)を決めてMoveWindow関数かSetWindowPos関数でコントロールを移動させました。

 

しかし、これは余り利口な方法ではなく、私も少し知恵がついて、最近のプログラム、例えばBatchGood等では、

 

(1)親ウィンドウのメンバー変数等値を保持できる変数を設け、そこにWM_SIZEが呼ばれる都度(lParamの上位と下位にそれぞれクライアントエリアの高さ、幅が入っている)、クライアントエリアの幅、高さを記録するようにする。

(2)WM_SIZE処理で最後に記録された値と今回のlParamの値を比較して幅、高さの「差分」を算出し、上下左右へ動かすもの、サイズを変更するもの等に区分してその「差分をAnchor以外に反映」して再計算し、

(3)MoveWindow関数かSetWindowPos関数でコントロールを移動させる

 

ようにしました。この結果コードは短縮され、同じ動作をするコントロールは一括処理され、↓のとおり処理内容もよくわかるようになりました。(BCCMakerのOnSize関数も後に書き換えました。)

 

【BCCSkeltonの"Anchors"処理-BatchGoodの例】

bool BATCHGOODDLG::OnSize(WPARAM wParam, LPARAM lParam) {

    //ツールバー再設定
    TBar.AutoSize();
    //ステータスバー再設定
    SBar.AutoSize();
    //コントロールの位置、サイズ変更
    RECT rec;    //矩形取得用
    POINT pt;    //スクリーン座標変換用
    int w, h;    //幅、高さ計算用
    
int diffx = LOWORD(lParam) - m_Width;    //前回と今回の差分
    int diffy = HIWORD(lParam) - m_Height;    //前回と今回の差分

    m_Width = LOWORD(lParam);                //今回の幅
    m_Height = HIWORD(lParam);                //今回の高さ
    
//左右移動のみ
    //IDC_DETAIL
    GetWindowRect(GetDlgItem(m_hWnd, IDC_DETAIL), &rec);    //ウィンドウ位置取得
    w = rec.right - rec.left;
    h = rec.bottom - rec.top;
    pt.x = rec.left;
    pt.y = rec.top;
    ScreenToClient(m_hWnd, &pt);
    MoveWindow(GetDlgItem(m_hWnd, IDC_DETAIL), pt.x + diffx, pt.y, w, h, TRUE);

    //幅変更のみ
    //IDC_BCC32OPTION、IDC_BRC32OPTION、IDC_INCLPATH
    
for(int i = IDC_BCC32OPTION; i <= IDC_INCLPATH; i++) {
        GetWindowRect(GetDlgItem(m_hWnd, i), &rec);    //ウィンドウ位置取得
        w = rec.right - rec.left;
        h = rec.bottom - rec.top;
        pt.x = rec.left;
        pt.y = rec.top;
        ScreenToClient(m_hWnd, &pt);
        MoveWindow(GetDlgItem(m_hWnd, i), pt.x, pt.y, w + diffx, h, TRUE);
    }
    
//幅高さ変更
    //IDC_OUTPUT
    GetWindowRect(GetDlgItem(m_hWnd, IDC_OUTPUT), &rec);    //ウィンドウ位置取得
    w = rec.right - rec.left;
    h = rec.bottom - rec.top;
    pt.x = rec.left;
    pt.y = rec.top;
    ScreenToClient(m_hWnd, &pt);
    MoveWindow(GetDlgItem(m_hWnd, IDC_OUTPUT), pt.x, pt.y, w + diffx, h + diffy, TRUE);
    //クライアントエリアを再描画
    InvalidateRect(m_hWnd, NULL, TRUE);
    return TRUE;
}

 

しかし、C#はこんな面倒なコードを書くことなく、コントロールオブジェクトのAnchorプロパティに好きな「錨の値」を入れるだけで同じ動作をします。

    button1.Anchor = (AnchorStyles.Top | AnchorStyles.Right);

(以下は同じレイアウトのメインフォームに、ツリービューとリストビューを入れたサンプルです。どっちも簡単でした。)

 

確かにC#に慣れたら、C++に帰れなくなるかも、と感じてしまいますね。

 

Visual Studioを頑なに使わず、MSCompAss Ver 1.3だけでプログラムを組んでいますが、Visual Studio(含むMSBuilder等の裏方)が(初心者プログラマーが知らずに)助けている領域がわからないと途端に迷宮に入ってしまします。

 

1.コモンダイアログ-「ファイルを開く」ダイアログと「フォールダーを選択する」ダイアログ

C++ではコモンダイアログ(厳密にいえば、フォールダーを選択するダイアログはShell サービス<SHBrowseForFolder>だけど...汗;)でおなじみですね。これをC#ではどうやって行うか?(だって、アプリを作ると直ぐに必要になるので)、に関心がわき調べてみたらとても簡単でした。例えば、以下が簡単な例です。

using System;
using System.Windows.Forms;

class Sample
{
    public static void Main()
    {
        //フォールダーを選択するダイアログ
        
FolderBrowserDialog fbDialog = new FolderBrowserDialog();
        // ダイアログの説明文を指定する
        fbDialog.Description = "フォールダーを選択してください";
        // デフォルトのフォルダを指定する
        fbDialog.SelectedPath = @"C:";
        // 「新しいフォルダーの作成する」ボタンを表示する
        fbDialog.ShowNewFolderButton = true;
        //フォルダを選択するダイアログを表示する
        if (fbDialog.ShowDialog() == DialogResult.OK)
        {
            Console.WriteLine(fbDialog.SelectedPath);
        }
        else
        {
            Console.WriteLine("キャンセルされました");
        }
        // オブジェクトを破棄する
        fbDialog.Dispose();
        //ファイルを開くダイアログ
        
OpenFileDialog ofDialog = new OpenFileDialog();
        // デフォルトのフォルダを指定する
        ofDialog.InitialDirectory = @"C:";
        //ダイアログのタイトルを指定する
        ofDialog.Title = "ダイアログのタイトル";
        //ダイアログを表示する
        if (ofDialog.ShowDialog() == DialogResult.OK)
        {
            Console.WriteLine(ofDialog.FileName);
        }
        else
        {
            Console.WriteLine("キャンセルされました");
        }
        // オブジェクトを破棄する
        ofDialog.Dispose();
        Console.ReadKey();
    }
}

 

しかし、これをこのまま走らせると

ダイアログが現れないばかりか、プロログラムが暴走

します。

なんで!?!(泣き;)

 

これは結構(原因追及に)苦しみました。

結局、他のサンプルを探したり、このサイトでの記事()を参考に、エントリーポイントのpublic static void Main()の前に    [STAThread]を付けたら、

<つまり...>

class Sample
{
     [STAThread]

    public static void Main()
    {

簡単に解決しました。

(元々STAが前提で、Visual Studioが下ごしらえで用意してくれる環境ではなく、なんでも自分でやらなければならないMSCompAss環境の)初心者には何も書かれていないとキツイですね。

:(記事からの引用)「自分でCOMを直接使っていないとしても、.NET Frameworkの一部の機能はCOMを使用しているため、そのような機能を使用する場合は、必ずSTAにする必要があります。STAでないと使用できない.NET Frameworkの機能には、以下のようなものがあります。(下記の「参考」にあるページ等を参考にしました。)
ドラッグ&ドロップ機能
クリップボード関係の機能

OpenFileDialog、SaveFileDialogのようなFileDialogの派生クラスや、FolderBrowserDialogクラスによるダイアログの表示
WebBrowserコントロール
IMEの使用
RichTextBoxコントロールの一部の機能(未確認)
リフレクションを利用したメソッドの呼び出し(.NET Framework 1.0 のみ?)

 

2.リソースの利用

簡単で、しかも分かりやすいので定評がある「猫でも」が、とうとうリソース(イメージ-)を扱うようになりました。となると私も何とかしてリソースを扱わなければなりません。

:C#で扱うリソースは、文字列、イメージ、アイコン、オーディオ、ファイル等です。リソースはファイルへのリンク(これは外部*.resxファイルの参照という意味ではないようですが、まだよくわかっていません-いずれにしてもあまり使われないようです)または埋め込み(こちらの方がより一般的ということです)の二種類があるそうです。また、Visual Studioでリソースを追加すると、プロジェクトの名前空間にResourcesクラスが自動で生成されるそうで、取得するには単に、

例:    Bitmap bitmap1   = Namespace.Properties.Resources.Image1;    // イメージ

等とするだけでよくなるようです。しかし、「手書き派」はこのような簡単な方法がないので自分で調べて、リソースの埋め込み、その取得を行わなければなりません。

 

ということで、WEBを漁るのですが載っているのは皆「Visual Studioでどうリソースを扱い、埋め込み、プログラムで呼び出すのか?」という説明だけで、「Visual Studioを使わずに手書きでプログラムを書く」大バカ者を相手にした記事は無いです。

仕方なく、書かれている記事から参考になるような情報を引き出して、自分で実験と検証を繰り返すしかありません。以下に私の「本日の大冒険」を順に概説します。

 

(1)「*.resx」リソースファイルを書く

MicrosoftのC# コンパイラー(csc.exe)は、単にプログラムアイコンをつけるだけの"/win32icon:(ファイル名)"オプションを除けば、"/win32res:(ファイル名)"オプションで一応Win32 リソースを扱うことは扱えますがプログラミングは超面倒であり、事実上袂を分かったといえるので、リソースを扱うには"/resources:(ファイル名)"オプションでリソースファイル()を埋め込むようです。

:最初私は誤解していて"*.resx"というXAML(注の注)ファイルを埋め込むのだと思っていました。しかし、C#のリソースファイルは多種あり、プログラムに埋め込むのはバイナリーファイルである"*.resources"ファイルで、XAMLで書かれた"*.resx"ファイルはテキストファイルです。

注の注:になる「XML(Extensible Markup Language)」がWWWコンソーシャムによるマークアップ言語の総称、XMAL(Extensible Application Markup Language)はそれから派生したMicrosoftの開発用マークアップ言語という理解です。

 

ということで、どうすればよいのかな、と思っていたら、親切な Microsoft は文字列や画像などをresxファイルにリソースとして書き込むResXResourceWriterクラスとAddResourceメソッドを提供していました。()これを使ってresxファイルを作成するテストプログラムを作り、たったのjpegの「猫」画像一枚で長々としたresxリソースファイルを作成しましたが、これを(馬鹿な私は)"/resource:"オプションに埋め込んでも画像を呼び出すことができません。

:これは午後に分かったのですが、XAMLテキストのresxファイルではなく、バイナリーのresoucesファイルに書き込むResourceWriterクラスとAddResourceメソッドも提供されていました。

 

「何とかして読みだせない」かということで、resxファイルを読み込んでリソースを使うプログラム迄はたどり着けました。

/* --- 外部リソースファイルから画像を読み込む --- */
        string resx_fn = @"C:\Users\ysama\Programing\C# Programing\Samples\resources\ResWriter.resx";
        //ResXResourceSetを作成する
        System.Resources.ResXResourceSet resx = new System.Resources.ResXResourceSet(resx_fn);
        System.Drawing.Bitmap img = (System.Drawing.Bitmap)resx.GetObject("SampleImage");
        Image = img;
        //閉じる
        resx.Close();

 

しかし、これはあくまでプログラムから外部のresxファイルを読んで、外部のjpegファイルにたどり着いてそれを読み込む、というもので「プログラムに埋め込まれたリソースを使う」ことはできませんでした。

 

その後、あれやこれや試行錯誤を繰り返し、Microsoftの"resgen.exe"というツールがresxテキストファイルをresourcesバイナリーファイルに転換してくれることがわかり、早速旧Win 10マシンのSKDフォールダーから調達。作成したresxファイルからresourcesファイルを作り、プログラムに埋め込むことはできるようになりました。(

:これは↑で書いた通り、ResXResourceWriterクラスのAddResourceメソッドの他、ResourceWriterクラスAddResourceメソッドで直接作成できることを知り、テストプログラム(ResourceWriter.cs)に機能を追加しました。

 

問題はプログラムに埋め込んだバイナリーリソースデータをどうやってプログラムが呼び出すか、ということですが、これが大変で調べまくった後、疲れて「今日は終了しよう」と思ったのですが、結局Microsoft Docsを(大分知識を得たので)読み直し、何とか埋め込んだバイナリーリソースを読みだして表示するテストプログラム(ReadResource.cs)でやっとこさ成功しました。

端無くも私のまともなC#アプリ第2号()であるReadResource.exeです。

:第1号は↑に書いたResourceWriter.exeという全く同じフォームのリソース書き込みアプリです。

 

あ~、疲れた!!!

これから、私はDewers 12年物のハイボールで打ち上げです!!!

 

ps. なお、この猫画像はBCCSkeltonでGDIPlusのクラスを作成してAlbum(アプリ)を作った時にWEBで取得したフリー画像ですが、この猫ちゃん、単純な茶雉ですがかわいいので気に入っています。

 

C#の学習用に作った(Microsoft Compilers ..., namely csc.exe and vbc.exe ... Assistantのつもりの)MSCompAssですが、最初はコンソールプログラム用で毎回オプションを設定する(というか、デフォールトがコンソールなので何もしないでよかった)設計でしたが、フォームを扱うようになると毎回設定するのが面倒になったので、ソースファイルをコンパイルする際にコンパイルオプションを「ソースファイル名.opt」というファイルで出力して記録し、ソースライフを読み込む際に直前のコンパイルオプション(if any)を読み込むようにしました。

また、この間C#でDLLを開発するのが超簡単だということがわかったので "/reference:" オプションを追加しました。

ついでに、デフォールトが小さいウィンドウでファイル名がすべて見えないのでファイル名にツールチップも追加しました。

 

「猫でも」のフォーム編で使っていますが、取り敢えず正常に動いています。(サンプルとして入れている、BCCForm and BCCSkeltonパッケージのアップ依頼は出していますので、いずれ更新されると思います。)