昨日打ち込んだ「猫でも」のサンプルは、SDIウィンドウにサイズ固定の矩形と文字列を描画するものでした。

 

しかしこのままでは面白くないので、ウィンドウサイズを変えると矩形と文字列(フォントサイズ)もつられて変化するように改造しようと考えました。

この場合、C++プログラマーはWM_SIZEメッセエージを捕まえてlParamのLOWORD(幅)とHIWORD(高さ)を取得して、それをもとに描画サイズを変更しようとします。ところが高級言語C#ではメッセージループ等なく()、(描画の場合のOnPaintイベントのように)WM_SIZEのイベントを発生させるのではないかと、OnSizeとかOnResizeとかのイベントを探ってみましたが見当たりません。途方に暮れていたら「あっ、C#ではこういうものはプロパティで取得するのかな?」ということで(フォーム.)Sizeプロパティを調べたらそれはWindowサイズで、更に調べて(フォーム.)ClientRectangle.Sizeで随時クライアントエリアのサイズを取得することができることがわかりました。(結構な探索時間がかかりましたよ。)

:調べてゆく過程で、↓のvoid WndProcメソッドがあることがわかりました。これを使うとC++的プログラミングができますが、あえてC#を下級化することはないのよほどの変態的処理でなければ使わないのでしょうね。以下はこのメソッドをオーバーライドしてWM_CREATEを拾って処理をする場合のコードです。

    protected override void WndProc(ref Message message)
    {
      base.WndProc(ref message);
      if (message.
Msg == WM_CREATE) {
        (処理);
      }
    }

 

以下が改造されたサンプルプログラムのコードです。赤字が改造部分で、たったこれだけでウィンドウサイズ変更処理ができました!

// drawstring02.cs

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Diagnostics;

class drawstring02 : Form
{
    public static void Main()
    {
        drawstring02 d2 = new drawstring02();
        Application.Run(d2);
    }

    public drawstring02()    //コンストラクター
    {
        Text = "猫でもわかるプログラミング";
        BackColor = Color.White;
    }

 

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);

        Size size = ClientRectangle.Size;
        Graphics g = e.Graphics;
        string str = "今日はよい天気です。\r\n" +
            "しかし明日もよい天気かどうかはわかりません。" +
            "明日は、明日の風が吹きます。";
        Font ft = new Font("MS ゴシック",
size.Width / 20);
        RectangleF rf = new RectangleF(10F, 10F, (size.Width - 20), (size.Height - 20));
        g.DrawRectangle(new Pen(Color.Blue), 10, 10, (size.Width - 20), (size.Height - 20));
        g.DrawString(str, ft, Brushes.Black, rf); 
    }
}

 

ここで「では、コード量を減らすためのBCCSkeltonで同様のものを書いたらどうなるのか?」という疑問が生じ、早速やってみました。(注)

注:最短のコードはBCCSkeltonのCANVASクラスを使う必要がありますが、C#では仮想ウィンドウを使っていないので、一応BCCSkeltonもOnPaint関数(WM_PAINTメッセージ処理)でWin32 API のみのコードもコメントで書いておきました。

 

//////////////////////////////////////////
// TestCat.h
// Copyright (c) 11/05/2022 by BCCSkelton
//////////////////////////////////////////
//BCCSkeltonのヘッダー-これに必要なヘッダーが入っている
#include    "BCCSkelton.h"
//リソースIDのヘッダー
#include    "ResTestCat.h"

/////////////////////////////////////////////////////////////////////
//CMyWndクラスをCSDIクラスから派生させ、メッセージ用の関数を宣言する
/////////////////////////////////////////////////////////////////////
class CMyWnd : public CSDI
{
public:    //以下はコールバック関数マクロと関連している
    //2重起動防止用のMutex用ID名称
    CMyWnd(char* UName) : CSDI(UName) {}
    //メンバー変数
    int    m_Width;        //クライアントエリア幅
    int    m_Height;        //クライアントエリア高さ
    //メニュー項目、ダイアログコントロール関連
    //ウィンドウメッセージ関連
    bool OnSize(WPARAM, LPARAM);
    bool OnPaint(WPARAM, LPARAM);
    bool OnClose(WPARAM, LPARAM);
};

////////////////////////////////////////////////////////////////////////
//派生させたCMyWndクラスのインスタンスとコールバック関数(マクロ)の作成
//主ウィンドウはダイアログと違い、コールバック関数は一つしか作れない
////////////////////////////////////////////////////////////////////////
CMyWnd TestCat("TestCat");    //ウィンドウクラスインスタンスの生成

BEGIN_SDIMSG(TestCat)    //ダイアログと違い、コールバック関数名を特定しない
    //メニュー項目、ダイアログコントロール関連
    //ウィンドウメッセージ関連
    ON_SIZE(TestCat)
    ON_PAINT(TestCat)
    ON_CLOSE(TestCat)
END_WNDMSG


//--- CANVASを使用 ここから ---
///////////////////////
//仮想ウィンドウの作成
///////////////////////
CANVAS cvs;

//--- CANVASを使用 ここまで ---

//////////////////////////////////////////
// TestCatProc.h
// Copyright (c) 11/05/2022 by BCCSkelton
//////////////////////////////////////////

/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//ウィンドウメッセージ関数
/////////////////////////////////
bool CMyWnd::OnSize(WPARAM wParam, LPARAM lParam) {

    m_Width = LOWORD(lParam);
    m_Height = HIWORD(lParam);

//--- CANVASを使用 ここから ---
    //仮想ウィンドウの初期化(一回だけ実行)
    static bool flag = TRUE;
    if(flag) {
        cvs.SetCanvas(m_hWnd);
        flag = FALSE;
    }
    //矩形描画
    cvs.Clear();
    cvs.Color(9);
    cvs.Box(10, 10, m_Width - 20, m_Height - 20, 0);
    cvs.Color(0);
    //メッセージ描画
    char* str = "今日はよい天気です。\r\nしかし明日もよい天気かどうかはわかりません。明日は、明日の風が吹きます。";
    RECT rec = {12, 12, m_Width - 24, m_Height - 24};
    cvs.PrintTextEx(str, &rec, RGB(0, 0, 0), "MS ゴシック", m_Width / 20, DT_TOP | DT_LEFT | DT_WORDBREAK | DT_NOCLIP);

//--- CANVASを使用 ここまで ---
    return TRUE;
}

/* --- CANVASを不使用 ここから ---
bool CMyWnd::OnPaint(WPARAM wParam, LPARAM lParam) {

    //描画のための構造体、ハンドル変数の宣言と初期化
    PAINTSTRUCT paint;
    HDC hDC = BeginPaint(m_hWnd, &paint);
    HPEN hPen = (HPEN)CreatePen(PS_SOLID, 1, RGB(0, 0, 255));
    HPEN hOldPen = (HPEN)SelectObject(hDC, hPen);
    HBRUSH hBrush = (HBRUSH)CreateSolidBrush(RGB(255, 255, 255));
    HBRUSH hOldBrush = (HBRUSH)SelectObject(hDC, hBrush);
    //矩形描画
    Rectangle(hDC, 10, 10, m_Width - 20, m_Height - 20);
    //メッセージ描画
    char* str = "今日はよい天気です。\r\nしかし明日もよい天気かどうかはわかりません。明日は、明日の風が吹きます。";
    HFONT hFont = CreateFont(m_Width / 20, 0,
                    0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
                    DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
                    DEFAULT_QUALITY, DEFAULT_PITCH, "MS ゴシック");
    HFONT hOldFont = (HFONT)SelectObject(hDC, hFont);
    SetTextColor(hDC, RGB(0, 0, 0));
    RECT rec = {12, 12, m_Width - 24, m_Height - 24};
    DrawText(hDC, str, -1, &rec, DT_TOP | DT_LEFT | DT_WORDBREAK | DT_NOCLIP);
    SelectObject(hDC, hOldFont);
    DeleteObject(hFont);
    //宣言したペンとブラシのメモリーを開放し、元に戻す
    SelectObject(paint.hdc, hOldBrush);
    DeleteObject(hBrush);
    SelectObject(paint.hdc, hOldPen);
    DeleteObject(hPen);
    EndPaint(m_hWnd, &paint);
    return TRUE;
}
  --- CANVASを不使用 ここまで --- */

//--- CANVASを使用 ここから ---
bool CMyWnd::OnPaint(WPARAM wParam, LPARAM lParam) {

    PAINTSTRUCT paint;
    cvs.OnPaint(BeginPaint(m_hWnd, &paint));
    EndPaint(m_hWnd, &paint);
    return TRUE;
}

//--- CANVASを使用 ここまで ---

bool CMyWnd::OnClose(WPARAM wParam, LPARAM lParam) {

    if(MessageBox(m_hWnd, "終了しますか", "終了確認",
                    MB_YESNO | MB_ICONINFORMATION) == IDYES)
        //処理をするとDestroyWindow、PostQuitMessageが呼ばれる
        return TRUE;
    else
        //そうでなければウィンドウではDefWindowProc関数をreturn、ダイアログではreturn FALSEとなる。
        return FALSE;
}

//////////////////////////////////////////
// TestCat.cpp
//Copyright (c) 11/05/2022 by BCCSkelton
//////////////////////////////////////////
#include    "TestCat.h"
#include    "TestCatProc.h"

////////////////
// WinMain関数
////////////////
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    LPSTR lpCmdLine, int nCmdShow) {

    //2重起動防止
    if(!TestCat.IsOnlyOne()) {
        HWND hWnd = FindWindow("MainWnd", "猫でもわかるプログラミング");
        if(IsIconic(hWnd))
            ShowWindow(hWnd, SW_RESTORE);
        SetForegroundWindow(hWnd);
        return 0L;
    }

    //ウィンドウ登録 - Init(ClassName, hInstance, WndProc, "IDM_MAIN",
    //                        (以下省略可)MAKEINTRESOURCE(IDI_ICON), IDC_ARROW, Brush)
    TestCat.Init("MainWnd", hInstance, SDIPROC, "", MAKEINTRESOURCE(IDI_ICON));

    //ウィンドウ作成と表示-Create(WindowTitle, (以下省略可)Style,
    //                        ExStyle, hParent, hMenu, x, y, w, h)
    if(!TestCat.Create("猫でもわかるプログラミング"))
        return 0L;

    //メッセージループに入る
    return TestCat.Loop();
}

 

いやはや、如何にC#が高級言語か、ということを見せつける差ですね。一応BCCSkeltonの実行画面も載せておきます。(システムアイコンが違いますね。)

 

一方、段々とC#に慣れてくるとC++のプログラミングが細部まで、細かくプログラマーが気を払わないとならないので大変だなと感じ始めました。実際今回の↑のサンプルを書く時にも結構忘れていることが多く、時間がかかりました。最後はC++でプログラムを書けなくなっちゃうのじゃないか、と心配です。

 

しかし、一番印象的であったのはBCCSkeltonの exe ファイルは約78KB(Win32 AP!-77.5KB、CANVAS-78KB)であるのに対し、C#の exe ファイルは約5KBと極めてコンパクトなことです。C#は自分のランタイムを持たず、OSの機能に依拠していることがわかります。

 

「猫でも...」の卒業制作として、(でもないのですが...)ウェブで目に付いた、C#によるDLL作成記事をもとに、簡単なDLLを作ってみました。

 

【DLL側】

単なる四則演算関数を提供するクラスDllSample01を持つDllSampleネームスペースを作ります。

 

///////////////////
// C# DLL Sample01
///////////////////

namespace DllSample
{
    public class DllSample01
    {

        public int Add(int a, int b)
        {
            return a + b;
        }

        public int Ded(int a, int b)
        {
            return a - b;
        }

        public int Mul(int a, int b)
        {
            return a * b;
        }

        public int Div(int a, int b)
        {
            return a / b;
        }
    }
}


これをMSCompAssで「出力オプション」を「/target:library」にしてコンパイルするだけです。(C++のようにDllMainなどのエントリー関数は不要です。)

 

【DLL呼び出し側】

DLL呼び出し側はネームスペースを使える(using)ようにするだけで、あとはクラスのインスタンスを作れば自由に関数(メソッド、か)を使えるようになります。

 

////////////////////
// Test Program for
// C# DLL Sample01
////////////////////

using System;
using DllSample;

public class SampleTest01
{
    static void Main()
    {
    
        try
        {
            DllSample01 ds01 = new DllSample01();
            Console.WriteLine("I was born in {0} and as I am {1}, it is {2} now.", 1954, 68, ds01.Add(1954, 68)); // DOB + Age
            Console.WriteLine("I am now {0} yeard old and was {1}, {2} years ago.", 68, ds01.Ded(68, 20), 20); // Age, Age - 20
            Console.WriteLine("My grand son, Thoma who is now {0} years of age, has to live {1} times as long as my age, {2}.", 2, 34, ds01.Mul(1, 68)); // DOB + Age
            Console.WriteLine("I've experienced {0} times of Hourse Years, as it comes once 12 years.", ds01.Div(68, 12)); // Age / 12
            Console.ReadKey();
        }
        //すべての例外をキャッチする
        catch(System.Exception ex)
        {
            //例外の説明を表示する
            Console.WriteLine("System.Exception Check - すべての例外をキャッチする");
            Console.WriteLine("\tMessage: {0}", ex.Message);
            Console.WriteLine("\tSource: {0}", ex.Source);
            Console.WriteLine("\tGetType: {0}", ex.GetType());
            Console.WriteLine("\tTargetSite: {0}", ex.TargetSite);
            Console.ReadKey();
        }
    }
}
 

これだけです。(↑の例ではエラーを出すのが怖くてtry - catchを使っています。)ただし、コンパイルの際には(DLLへの)「参照」を行うために、コンパイラーオプションに「/reference:(ファイルパス、名)」を追加する必要があります。(MSCompAssではオプションダイアログの「その他オプション」に入力ことが必要です。MSCompAssはソースファイルのあるディレクトリーでコンパイルするので、パスを長々と書く必要はありません。)

 

あまりに簡単に書けるので、C#のDLLをC++で利用しようかな、と思ったのですが、そんなに簡単な訳はなく、呼び出し規約の問題や引き渡しデータの問題等があり、(Visual StudioのC++でさえ呼び出すのが大変なのに)フリーのEmbarcadero C++のプログラムから呼び出そうとすれば長~い時間がかかりそうなのですぐに断念しました。

 

それにしても「高級言語」C++は簡単ですね。Visual StudioというIDEで効率よくプログラミングできる現在は別世界のようです。

 

今朝「猫でもわかるプログラミング」のコンソール編を卒業しました。

今までの学習経験から言うと、C#ってCやC++というハードウェアに近い、割と危険なこともできる言語からは程遠い高級言語感があり、「C(++)文法に慣れた人が使うVisual Basic」的な印象です。

 

事実、MSCompAssでコンソールプログラム(すなわち多くの場合、逐次処理)を組むだけの経験に限って言えば、(前にも書いたように)ROM Basicでプログラミングをしている錯覚を覚えます。

勿論、Cに近い文法やより高度の条件分岐や制御に加え、(ある意味C++を超えた)クラス(class)、構造体(struct-単にclassの値型のようで、すでにC(++)の構造体ではありません)、インターフェース(クラスの抽象化か?)、プロパティ、インデクサ(ー)、イベント処理や例外処理をカバーしており、結構楽しくプログラミングができる言語だと感じます。

 

一方、ウィンドウズプログラミング(未だフォーム編が現在進行形なのdせすが)についていえば、リソース関係はXMLでの記述のようだし、Win32 リソースの再利用は簡単ではないし、WYSWYG環境としてのVisual Studioがないときちんとした大き目のプログラム開発は困難でしょう。

とはいえ、自分で「どうなっているのかな?」と好奇心を持ち、楽しみながらプログラミングをするなら「手書き環境」でも結構いけるんじゃないでしょうか?(注1)私はコツコツとMSComAss + Sakura Editorだけで当分遊んでみるつもりです。

注1:C#の学習でこのサイトと遭遇し、VScode + .NET coreの環境設定動画を見ましたが、"Hello, World!"を打ち出すだけにこれだけ分からない世界を彷徨するのかと思うと、矢張り退けますね。

 

なお、C++プログラミングの方はまだよいネタがありません。C#ツールのようなものはVisual Studio Community Editoinがあるので無意味(注2)ですし、今更バイオリズムや家計簿(注3)もないと思うので。もう少し考えてみます。

注2:BCCForm and BCCSkeltonを作った20年前は、Microsoft の Visual C++ なんて(確か)6万円程度だったし、フリーの Windows プログラミング環境は Borland C++ Compiler くらいしかなく、それもMS-DOS コマンドツールベースだったので苦心してウィンドウベースの開発環境を自作したのですが、現在は Visual Studio Community Editoin がフリーで使えるので自作する必要は全くありません。(しかし、その為に"Hello, World!"で挫折する人も多いのではないかな?)

注3:昔々はPCを買うと、OSの代わりにROM Basicが入っていて、記憶媒体がカセットテープだったので、サンプルプログラムは取扱説明書に書いてあった!その代表格が「バイオリズムや家計簿」でした。

 

大分ご無沙汰していますが、将に今日の気分は↑のタイトルの通り。(これは大昔、私がまだ小学生の時にトム・ジョーンズという英国の歌手が歌っていたバート・バカラックとハル・ディヴィッドの曲で、同名のウッディ。アレンの映画の主題歌です。)

和訳は「何かいいことないか、子猫ちゃん?」

 

以下現状。

・C++で何かまたサンプルプログラムを作りたいのですが、「ひらめき」が降りてこず、MSComAss以降停滞しています。

・C#は、まだC++との共通点、相違点を踏まえて、コンソールプログラムサンプルを学習しています。

・本日平山夢明さんの「Diner(ダイナー)」という小説を読み終わりました。(その前は村上春樹さんの1Q84を6冊。)まだ、ストックの小説がありますが、またBook Offに出掛けようかと思っています。

・映画やドラマは大分ネタが尽きてきており、Amazon Primeでも「これっ!」という感じのものは少ないです。(英語ものを見ていましたが、韓流に移行し、最近は日本映画も捨てられないな、と思っています。)

・毎朝のウォーキングと車中で聞く音楽をiTuneにまとめていますが、今日はまたまた「懐メロ」の高中正義と山下達郎の曲を編集、追加しました。(この間は何故か来生たかおの「夢の途中」が頭に湧いてきて、渦巻いて困りました。後、初期のHerbie HancockのWaltermelon manなんかも。)

 

ウ~ン、何かパッとしませんね。毎日のウォーキング、朝風呂等で色々と考えるのですが、閃きがありません。まぁ、もう少し頑張ってみましょう。

 

(何度か改良して今は健気に動いている)MSCompAssを使ってC#プログラムサンプルを見て学習していますが、前回は不覚にも「VB.NETも構造は同じようなのかな?」と好奇心を出したのが運の尽きで、Visual Basic(注)へのコンバーターが不十分で、その修正で時間を食いました。

注:元々Basic言語は、Microsofttのお家芸(昔はMicrosoft Basic, MS BasComなどがありました)ですが起源はダートマス大といわれています。昔8 bit、16 bit PCを買えば必ずROM Basicがついてきて、電源を入れたら即Basic環境という、OSのような位置づけでしたので、利用者はすべて使わなければならなかったのですが...MSの陰謀か?...現在はOSがウィンドウベースとなり、手続き型のBasic言語で生き残っているのはMicrosoftのVisual Basicや元祖ダートマス大のTrue Basicぐらいじゃないでしょうか?いずれにせよ、.NETになって従来のBasicとは似ても似つかなくなりましたが、コード(タイプ)量が多いので、関数型言語になれると敬遠したくなりますよね。

 

これに懲りてまたC#での学習に戻りましたが、2000年からC++のプログラミングをはじめ、Win32 APIのみでリソースエディターを書いた私には、矢張り

(1)C#でリソースをどう扱うのか?

(2)コントロールとかの定数を集めたヘッダーファイルはどう扱うのか?("#include"ってみたことないし。)

が疑問でした。それで今日、WEBで調べてみたのですが、

 

(1)については、まだよくわかっていないのですが、少なくとも

 ①C++で使う*.rcファイルや*.resファイルを扱うような記事は見いだせなかった。(C++のリソースは使わない。)

 ②リソースファイルはXMLのリソースファイル(*.resx)を使うのが一般的。

 ③いずれにしてもVisual Studioでのリソース作成、リソース管理を前提としたC#での利用が「常識」であり、

 ④結論としてC++でのリソース資産を使うような発想はあきらめた方がよい。

というものです。まだ正しいか否かわわかりませんが、恐らく当たらずとはいえ遠からず、ではないかと思います。

 

(2)については「すべてnamesupace(例の"using <DLL名>")で処理するので、"#include"は無い」そうです。

 

ということで、きっちりとしたC#によるウィンドウズプログラムを書こうと思ったら、矢張りVisual Studio無しではお話になりませんね、ということになります。

 

なお、この過程でC++プログラマーにはとても有益なサイトと巡り合えましたので紹介します。まさにポイントをついた説明で思わずうなづいてしまいました。

C++プログラマのためのC#入門

というわけで、余命長くない私が「C#に鞍替えして進もう」という考えは毛頭ないのですが、もともとの本ブログの目的である「ボケ防止」にはちょうど良いので、もう少しこのシリーズは続けてゆきましょう。(しかし、暇プロプログラム開発はBCCSkeltonベースでC++を使うことは、本ブログの性格からconst、です。)

 

同日追記:前にMSCompAssに関連して書いたように、C#コンパイラー(csc.exe)には"/win32res:<ファイルパス、名>"というオプションがあり、これによって"*.res"ファイルを実行プログラムに埋め込むことができます。しかし、組み込んだリソースをプログラムでどう使うか、についての記述が見つからず、一番近いのがこれかなと思う位のものしかありません。

しかし、これも実行プログラムに埋め込まれると"IDI_ICON"等のID名しかない(Dumpで見る限り)ので「これ」の例にあるようなID("MyNamespace.MyIcon.ico")をどうひねり出すのか、不明です。いずれにしてもC++ではWin32 APIの"LoadIcon"関数一つで済む処理がC#では結構面倒くさいことになることは確かです。

 

どうもWEBに出ていたコードに疑問が残るので、「いらないんじゃないか?」と思われるコードを整理してみました。そうしたらやっぱり動きました。

 

【習作C#プログラム】

// paintargs01.cs

using System;
using System.Drawing;
using System.Windows.Forms;

class paintargs01
{
    public static void Main()
    {
        Form f = new Form();
        f.Text = "猫でもわかるプログラミング";
        f.BackColor = Color.White;
        f.Paint += new PaintEventHandler(MyHandler);
        Application.Run(f);
    }

    static void MyHandler(object sender, PaintEventArgs e)
    {
        Graphics g = e.Graphics;
        g.DrawLine(new Pen(Color.Red), 10, 50, 280, 50);
    }
}
 

【再修正したVBプログラム】

Imports System
Imports System.Drawing
Imports System.Windows.Forms

Class paintargs01

 

    'Visual Basicの場合、PaintEventHandlerとしてイベントを宣言する
    ’Public Event Paint As PaintEventHandler→どう考えてもこれはいらないよね。
 

    Public Shared Sub Main()
        Dim f As Form = New Form()
        f.Text = "猫でもわかるプログラミング"
        f.BackColor = Color.White
        'f.Paint += New PaintEventHandler(AddressOf MyHandler)
        'C#の↑に相当するコードが以下3行となる
        'Dim PEH As PaintEventHandler
        'PEH = AddressOf MyHandler
        'AddHandler f.Paint, PEH
        'https://learn.microsoft.com/ja-jp/dotnet/visual-basic/language-reference/statements/addhandler-statement
        ’↓こう書けばよいだけじゃない?

        AddHandler f.Paint, AddressOf MyHandler
        Application.Run(f)
    End Sub

    Private Shared Sub MyHandler(ByVal sender As Object, ByVal e As PaintEventArgs)
        Dim g As Graphics = e.Graphics
        g.DrawLine(New Pen(Color.Red), 10, 50, 280, 50)
    End Sub
End Class

 

これでやっとコンバートしたっていう感じになりました。が、なんだか、今日一日すっごく疲れた。

【2022年10月19日修正追加】

コンバーターのvb.NETプログラムをMSComAssでコンパイルした所、C#のWM_PAINT割込み処理の追加部分がエラーになることが判明。これは問題だと午前中慣れないvb.NETの構文やステートメントを調べまくって、何とか動くようにできました。やはり僕にはC#、更に慣れたC++の方が楽だな。

 

Microsoft帝国の巨大資産であるVisual BasicとC#を、その領土以外でVisual StudioのIDEもMSBuildも一切使わないで「一からプログラムを作る」という無謀な試みをやってます。

 

一応昔Win32 APIプログラミングでお世話になった「猫でもわかるプログラミング(C#コンソール編、フォーム編)」でセコセコとやっていますが、じゃ、MSCompAssがcsc.exeのみならず、vbc.exeも使えるので同じようなへそ曲がりプログラミングサイトがないかな、と探してみました。しかし、さすがにBasicのMicrosoft(注)、機械作成コードでなければすべてのプログラムコードを作成するのが難しい領域迄到達しているようで、現在のIDEやMSBuild環境を離れてプログラミングしようなどという愚か者はいないようです。

 

さはさりながら、.NET時代に入り、C#とVisual Basicは「言語こそ違え、同じOSプラットフォームで同じ構造のインターフェースでやっていることはほとんど同じ」と感じていたのですが、それを体現している「暇人」が英語の世界にいました。

 

このサイト(Teleric Code Converter)は習作として作成したC#プログラムを簡単にVBプログラムに直してくれます。(もちろんVisual Studioで作成するような大きなプログラムではファイルが分割されてもいるでしょうし分かりませんが...)

 

【習作C#プログラム】

// paintargs01.cs

using System;
using System.Drawing;
using System.Windows.Forms;

class paintargs01
{
    public static void Main()
    {
        Form f = new Form();
        f.Text = "猫でもわかるプログラミング";
        f.BackColor = Color.White;

        f.Paint += new PaintEventHandler(MyHandler);

        Application.Run(f);
    }

    static void MyHandler(object sender, PaintEventArgs e)
    {
        Graphics g = e.Graphics;
        g.DrawLine(new Pen(Color.Red), 10, 50, 280, 50);
    }
}
 

【変換されたVBプログラム】

Imports System
Imports System.Drawing
Imports System.Windows.Forms

Class paintargs01
    Public Shared Sub Main()
        Dim f As Form = New Form()
        f.Text = "猫でもわかるプログラミング"
        f.BackColor = Color.White
        f.Paint += New PaintEventHandler(AddressOf MyHandler)
        Application.Run(f)
    End Sub

    Private Shared Sub MyHandler(ByVal sender As Object, ByVal e As PaintEventArgs)
        Dim g As Graphics = e.Graphics
        g.DrawLine(New Pen(Color.Red), 10, 50, 280, 50)
    End Sub
End Class
 

な~るほど、という感じですね。→じゃなかったです。

【修正追加版】↑のまま修正部分追加コメントです。

Imports System
Imports System.Drawing
Imports System.Windows.Forms


Class paintargs01

    'Visual Basicの場合、PaintEventHandlerとしてイベントを宣言する
    Public Event Paint As PaintEventHandler

    Public Shared Sub Main()
        Dim f As Form = New Form()
        f.Text = "猫でもわかるプログラミング"
        f.BackColor = Color.White

'        f.Paint += New PaintEventHandler(AddressOf MyHandler)
        'C#の↑に相当するコードが以下3行となる
        Dim PEH As PaintEventHandler
        PEH = AddressOf MyHandler
        AddHandler f.Paint, PEH

        'https://learn.microsoft.com/ja-jp/dotnet/visual-basic/language-reference/statements/addhandler-statement
        Application.Run(f)
    End Sub


    Private Shared Sub MyHandler(ByVal sender As Object, ByVal e As PaintEventArgs)
        Dim g As Graphics = e.Graphics
        g.DrawLine(New Pen(Color.Red), 10, 50, 280, 50)
    End Sub
End Class

 

まず初めにお詫びをいたします。C++プログラマーが陥るC#の罠にまんまとはまり、図らずもブログネタを作ってしまいました。

昨日書いたブログの中のC#でnewしたインスタンスをで遊んだ後、優しいMicrosoftお母さんがお片づけ(ガーベージコレクション)をしてくれるのですが、C++でnewして遊んだ後は自分でお片づけ(deleteによるメモリー領域の開放)をしないと厳格なC++お母さんに「メモリーリークだよ」としかられます。(昨日のブログのコードに本日赤字"delete mc[i];"を追加しておきました。自動開放はしないので、配列の開放(デストラクター)の順序はプログラミングによる、と言えます。)

 

あと、ちょっとびっくりしたのは、C#の実行ファイルの小ささです。(constructor01-destructor01.exeは5Kバイト弱)C++の方は145KBもあります。(注)

注:もともとbcc32cでコンソールプログラムをコンパイルするとランタイムを組み込むので結構大きくなります。ウィンドウズプログラムの方がOSのサービスを呼び出すだけなのでコンパクトです。OSを知り尽くしたMicrosoftが作ったC#でもそういうことが言えると思います。...それで思い出したのは、私がシンガポールでbcc55でC++を書いていた時にC#がリリースされ、当時はまだ今のように機能が豊富ではなく「プラットフォームはWindowsだけの、C++またはJavaのMicrosoft方言」なので当分見送ってよい、という話です。あれから20年10回のバージョンアップを経てクロスプラットフォーム化(.NET)に至ったんですねぇ。

 

さて、本日のお題は関数(おっとC#の場合はメソッドですね!)のオーバーロードです。「メソッドの名前と引数をメソッドのシグニチャ」という、とのことでシグニチャが 異なればオーバーロード(同じ名前のメソッドを持つこと...それってなんかかぶっている感じがしますが)可能とのこと。戻り値はどうなの?と思いましたが、Microsoftによれば「アクセス レベル、オプションの修飾子、戻り値、メソッドの名前、およびメソッド パラメーター」「のまとまりがメソッドのシグネチャ」なんですが、「メソッドのオーバーロードを可能にするために、メソッドの戻り値の型はメソッドのシグネチャには含まれません」とのことです。

 

今日も短いサンプルをつなぎ合わせて、↓のようにしてみました。

// overload01-02.cs
using System;
class Sq
{
    public static int sq(int x)
    {
        Console.WriteLine("intバージョンが呼ばれました");
        return x * x;
    }
    public static double sq(double x)
    {
        Console.WriteLine("doubleバージョンが呼ばれました");
        return x * x;
    }
    public static string sq(int x, ref string str)    //これは戻り値の違いがシグニチュァになるかテストし、ダメだったので、台2引数を追加したものです。
    {
        Console.WriteLine("stringバージョンが呼ばれました");
        str = (x * x).ToString();
        return str;
    }
}

class Plus
{
    public int m_x {get; set;}    //使いませんでしたが、為念コンパイラーが通るか試してみました。しかし、"public int m_x {get; set;} = 0;"という初期化はエラーになりますね。

    public int add(int x)
    {
        m_x += x;
        return m_x;
    }

    public int add(int x, int y)
    {
        m_x += x + y;
        return m_x;
    }

    public int add(int x, int y, int z)
    {
        m_x += x + y + z;
        return m_x;
    }

    public void Show()
    {
        Console.WriteLine("m_xの値は{0}です。", m_x);
    }
}

class Sample
{
    public static void Main()
    {
        // overload01
        Console.WriteLine("{0}の2乗は{1}です", 5, Sq.sq(5));
        Console.WriteLine("{0}の2乗は{1}です", 0.5, Sq.sq(0.5));
        string str = "";    //string str;だけだと「未割り当てのローカル変数 'str' が使用されました。」エラーとなる。
        Console.WriteLine("{0}の2乗は{1}です", 10, Sq.sq(10, ref str));
        // overload02
        Plus plus = new Plus();
        plus.Show();
        Console.WriteLine("m_x += {0}", 10);
        plus.add(10);
        plus.Show();
        Console.WriteLine("m_x += {0} + {1}", 10, 20);
        plus.add(10, 20);
        plus.Show();
        Console.WriteLine("m_x += {0} + {1} + {2}", 10, 20, 30);
        plus.add(10, 20, 30);
        plus.Show();
        Console.ReadKey();
    }
}

 

未だ慣れていないので、時々「(メモリー)未割り当てのローカル変数 'str' が使用されました。」エラーを出します。しかし、慣れすぎてnewを使っても、deleteしない環境にはなれましたがね。(笑)

 

今日のC#のお勉強はコンストラクターとデストラクター。いつものように、勝手にサンプルを書き換えてコンパイルします。

 

【Constractor-Destructor.cs】

// constructor01-destructor01.cs

using System;

class MyClass
{
    int x;
    int y;

    public void Show()
    {
        Console.WriteLine("x = {0}, y = {1}", x, y);
    }

    public MyClass(int a, int b)
    {
        x = a;
        y = b;
        Console.WriteLine("引数付きコンストラクタが呼ばれました(x = {0}, y = {1})", x, y);
    }
    public MyClass()
    {
        x = 0;
        y = 0;
        Console.WriteLine("引数無しコンストラクタが呼ばれました(x = {0}, y = {1})", x, y);
    }

    ~MyClass()
    {
        Console.WriteLine("デストラクタが呼ばれました(x = {0}, y = {1})", x, y);
    }
}

class Sample
{
    public static void Main()
    {
        Console.WriteLine("MyClassの配列を生成します");
        MyClass[] mc = new MyClass[10];
        for(int i = 0; i < 10; i++) {
            switch(i) {
            case 0:
                mc[i] = new MyClass(10, 20);
                break;
            case 1:
                mc[i] = new MyClass();
                break;
            default:
                mc[i] = new MyClass(i, i * 10);
                break;
            }
        }
        for(int i = 0; i < 10; i++) {
            mc[i].Show();
        }

        Console.ReadKey();
    }
}

 

ただこれだけのつまらないプログラム(注)ですが、C++でもFIFOでメモリーが解放(デストラクターの実行順序)されたかな、と?がついて、C++で同じコンソールプログラムを作りました。

注:しかし、↑の「MyClass[] mc = new MyClass[10];」というMyClassクラスの配列を宣言している段階ではまだMyClassインスタンスが作られず、さらに「new MyClass(<引数有無共>);」で実際にメモリーが割り当てられることがわかりました。

 

【Constructor-Destructor-cpp.cpp】

//////////////////////////////
//Constructor and Destructor
//////////////////////////////
#include    <stdio.h>
#include    <conio.h>    //getch()使用の為
#include    <iostream>    //cout、cin使用の為

using namespace std;    //iostream使用のため

class myclass {

    //メンバー変数-
デフォルトはprivate→C#も同じ
    int m_x = 0;
    int m_y = 0;
    //メンバー関数-アクセス修飾子をpublicにしている
public:
    myclass();
    myclass(int, int);
    ~myclass();
    void show();
};

//引数無しコンストラクター
myclass::myclass() {

    std::cout << "引数無しコンストラクタが呼ばれました(x = " << m_x << ", y = " << m_y << ")" << endl;
}

//引数付きコンストラクター
myclass::myclass(int a, int b) {

    m_x = a;
    m_y = b;
    cout << "引数付きコンストラクタが呼ばれました(x = " << m_x << ", y = " << m_y << ")" << endl;
}

//デストラクター
myclass::~myclass() {

    cout << "デストラクタが呼ばれました(x = " << m_x << ", y = " << m_y << ")" << endl;
}

void myclass::show() {

    cout << "x = " << m_x << ", y = " << m_y << endl;
}

//メイン関数
int main() {

    cout << "myclassの配列を生成します" << endl;
    //myclassクラスのポインター配列
    myclass *mc[10];
    for(int i = 0; i < 10; i++) {
        switch(i) {
        case 0:
        //引数付きコンストラクター
            mc[i] = new myclass(10, 20);
            break;
        case 1:
        //引数無しコンストラクター
            mc[i] = new myclass();
            break;
        default:
        //引数付きコンストラクター
            mc[i] = new myclass(i, i * 10);
            break;
        }
    }
    for(int i = 0; i < 10; i++) {
        mc[i]->show();

        delete [] mc[i];    //翌日修正。自分でネタ作ってしまった。C#にしてやられた。
    }
    getch();
    return 0;
}

//結局、出力は全く同じでした。(→myclass配列順にしているので当たり前ですが。)

 

ということで何も面白い話が無いように見えますが、このC++のプログラムを書いていて不思議な現象に出会いました。それはOSがパス、ファイルを認識していても、bcc32c.exeが同じファイルパス、名を認識できない、という現象です。以下はBatchGoodで作ったバッチファイルにソースファイルの存在確認のために、

dir "(ソースファイルパス)\(ソースファイル名)

の一行を加えたものです。

OS(Windows 11)はconstructor01-destructor01-cpp.cppという1,505バイトのファイルを認識していますが、bcc32c.exeは"no such file or directory"、"no input file"というエラーを出しています。これは場所を変えると認識しますので、パスの中にある'#'あたりが気に食わないのではないか、と勝手に想像しています。まぁ、そういう場合はパス名を変更するか、場所を移動させましょう。

 

大体毎日サンプルを1~数件目を通し、自分なりに応用問題として変更して、実際にコンパイルしています。

 

今日はC#のクラス定義とメンバー関数(注)です。短い例題がいくつかあるので、みーんなまとめて一つのプログラムにしてしまいます。(私が纏めたコードは末尾参照)

注:C#ではデータを仕舞うメンバー変数をフィールド、メンバー関数(「一連のステートメントが含まれているコードブロック」)をメソッドというようです。「えっ、データはプロパティではないの?」と思いましたが、プロパティは「オブジェクト内にあるフィールドの値を取得、または設定するための手段(メンバー変数の値の取得・変更を行うためのメソッドであるアクセサーを含む)」なんだそうです。

 

末尾にある本日の私のコードでは、クラスが二つあり、メソッドのテストのためのMyClass(紫)とMain関数があるSample(緑)に分かれています。青字はコメントで、今日のトピック赤字にしている

(1)"static"、

(2)"ref" と

(3)"unsafe"

です。

 

(1)"static"

C#のクラスにおいても、C++と同じく、static(静的)キーワードを使う場合、「クラスに記憶領域を確保して、クラス全体に共通の変数、関数」となります。(巻末サンプルで実験済)したがって、「クラス」という抽象的な仕様を定義して、現実の実態であるインスタンス(オブジェクト)を宣言して作っても、static 変数やstatic 関数は共用されることになります。

<C++の場合>

class foo

{

    static int x = 10;

    static bool xyz(int, char*);

};

 

foo bar;    //bar.xは10

foo zot;    //zot.xも同じ変数で10、これを他の値に代入するとbar.xの値も変わってしまう

bar.xyz(10);    //同様にこの関数内に保持する変数があると、同じクラスの他のインスタンスがそれを変えると意図せずに値が変わってしまう

<C#の場合>

C#の場合には「staticキーワードを付けた変数乃至関数の呼び出しは、『(インスタンス名).(変数、関数)』ではなく、『(クラス名).(変数、関数)』になる」ことがC++と異なります。(↑の例でいえば、"foo.x"や"foo.xyz(10)"と書くべきところ、"bar.x"や"bar.xyz(10)"と書くとエラーになり、間違いがすぐにわかります。)

これにより、C++よりプログラマーサイドでのポカミス率が下がると考えられます。

 

(2)"ref"

C#もCやC++と同じように「値渡し(call by value)」型の関数になっていますが、「参照渡し(call by reference)」も可能になっています。

CやC++の場合、ポインター('*' 間接演算子)を使えるので「ポインターを使った参照」が広く行われますが、C#は基本(危険な)ポインターを使った処理は行えず、ちょうどC++の'&'(アドレス演算子)をつけて参照渡しを行うように、"ref" キーワードを使って参照渡しを行うことができます。(丁度Visual Basicの "ByRef" キーワードのようですね。)末尾のサンプルを参照してください。

 

(3)"unsafe"

最後がC#におけるポインター処理の作法です。

C#でもポインターを使うことは不可能ではなく、

①ポインターを使う関数では宣言に"unsafe"キーワードを使う。

②ポインターを使う"unsafe"な関数を呼び出す側では、その処理を"unsafe"キーワードのブロック内で行う。

③更に、"unsafe"を使ったプログラムのコンパイルでは"/unsafe(+)"オプションをつける。

ことが必要になってきます。(末尾のサンプル参照)

 

詳しくは

アンセーフ コード、データへのポインター、および関数ポインター | Microsoft Learn

を見てください。

 

ps. なお、今日の学習中、MSCompAssのオプションに"/unsafe"と書き込んでもその効果がないので再度チェックしたら、手書きオプションが反映されていないことが判明しました。(ゴメンナサイ!)早速メインダイアログの手書きを廃止し、すべてオプションダイアログで入力する形にして、オプションダイアログに手書き用エディットコントロールを追加しました。(本日修正版アップ済→修正版で↓のサンプルをコンパイルテスト済です。)ということで、

今日は疲れたっ!

 

【今日の例題をまとめ、自分なりに変更したコード】

// method01-03.cs
using System;

class MyClass
{
    public void Show()
    {
        Console.WriteLine("MyClassのインスタンスmcのShowメソッドが呼ばれました");
    }
    public
static void stShow()
    {
        Console.WriteLine("MyClass共通のstatic Showメソッドが呼ばれました");
    }

    public int squared(int a)
    {
        return a * a;
    }

    public void swap01(
ref int x, ref int y)
    {
        int temp = x;
        x = y;
        y = temp;
    }

    public void swap02(
ref int x, ref int y)
    {
        
x ^= y;
        y = x ^ y;
        x ^= y;

    }

    
unsafe public void swap03(int *x, int *y)    //error CS0227: アンセーフ コードは /unsafe でコンパイルした場合のみ有効です。
    {
        *x ^= *y;
        *y = *x ^ *y;
        *x ^= *y;
    }
}


class Sample
{
    
// method01
    public static void Main()
    {
        MyClass mc = new MyClass();
        mc.Show();
    
// method02.cs
        MyClass.stShow();
    
// method03.cs
        Sample smp = new Sample();
        smp.Show1();
        Show2();
  
 // method04.cs
        int x = mc.squared(10);
        Console.WriteLine("squaredを使い、10の2乗 = {0}を求めました", x);
    
// swap01.cs
        x = 10;
        int y = 20;
        Console.WriteLine("「ref 変数」とtemp変数を使って、x = {0}, y = {1}をスワップします", x, y);
        mc.swap01(
ref x, ref y);
        Console.WriteLine("x = {0}, y = {1}になりました", x, y);
  
 //swap02
        Console.WriteLine("「ref 変数」と排他的論理和を使ってx = {0}, y = {1}をスワップします", x, y);
        mc.swap02(
ref x, ref y);
        Console.WriteLine("x = {0}, y = {1}になりました", x, y);
    
//swap03
        Console.WriteLine("「unsafe(コンパイラーは \"/unsafe+\" オプション)」と排他的論理和を使ってx = {0}, y = {1}をスワップします", x, y);
        //ポインターを使う場合はその処理部分をunsafeキーワードでブロック化する
        
unsafe {
            mc.swap03(&x, &y);
        }

        Console.WriteLine("x = {0}, y = {1}になりました", x, y);
        Console.ReadKey();
    }

    void Show1()
    {
        Console.WriteLine("SampleクラスインスタンスのShow1が呼び出されました");
    }

    
static void Show2()
    {
        Console.WriteLine("Sampleクラス共通のstatic Show2が呼び出されました");
    }
}

//上記コードのコンパイルには、csc.exeに"/unsafe"オプションを与えることが必要です。