前回、Windowsの歴史を振り返ってみました。

で、

思ったのは、WindowsというOSの本質ってなんだろう、ということ。

 

(1)マルチタスク、マルチスレッド

マルチタスクマルチスレッドというと、すぐウィンドウがオーバーラップするGUIのOSを思い浮かべますが、元々はプリエンプティブマルチタスクといえども8bitの時代からCUIベースで存在しました。(若い時は、富士通のFMシリーズでOS-9が走ると聞いてうらやましかったことを記憶しています。)また、プレゼンテーションマネージャーというGUIを起動することもできましたが、複数のDOSが走るOS/2がありました。いずれにしても、シングルタスク、シングルスレッドであった昔は憧れでも今はフツーに提供していますよね。でも、私(わたし)的にはこの側面がとてもありがたいと思っています。

 

(2)リソース、プロセス、接続・アクセス等管理

これは「OS論」のような話になり、ここまで広げると素人には荷が重いですよね。素人にはこちらの方がなじめます。また、カバーする領域が広いので、人によって切り口が異なり、却って迷宮に入っちゃいます。

 

(3)GUI

一般的にはこれが特徴的に映りますね。特に(日本では余りWin3.0がはやらなかったので)Windows 95では熱狂して受け入れられました。しかし、昔はCPUの力が弱く、メモリーも遅くて高価だった為、APIを含めて極めて多くのコンピューターリソースを費やすGUIには腰が引けていたというのが事実です。その後CPU性能が上がり、メモリーも潤沢且つ安価に手に入るにつれ、GUIのデザインも世につれ変遷(Windows 1.0~Windows 10)してきました。その結果、現在は余りコストやスピードに気にせずにGUIをエンジョイさせていただいてますね。

所でこのGUI、というかウィンドウですが、どのようになっているのでしょう?これはプロ目線過ぎて余り参考になりませんね。一方、これは簡単、というよりは雑ですね。Wikiでは、

GUIの仕組みは?

基本的には「デスクトップ」「ウィンドウ」「メニュー」「アイコン」「ボタン」など要素を組み合わせて構成され、それらをポインティングデバイスによって操作されるカーソルを通じて指示を与える。 端的に言うと、画面上のボタンや画像などを選択する事でリアクションを発生させる仕組みを総称してGUIと言う。

と書かれていますが、具体的に言うとどういうことかを考えてみました。

 

(1)マウスデバイスを動かす机上の移動量に合わせて、画面上にマウスカーソルを「動かす」。(背景画像の取り込みマウス輪郭のビット0画像と背景画像をANDでとる→マウス画像とその背景画像をORでとる[表示]保存した背景画像を表示[消去]

(2)マウスを使った処理操作の際に「スクリーン座標位置」と共に「どのボタンがどう押されたか」がPCに分かる。

(3)画面上のウィンドウは、

①ウィンドウの登録種類(例:オーバーラップウィンドウやボタンコントロール等)に合わせた形状等の一般情報を予め保持(ウィンドウクラス)し、

②特定のプロセスを担うメインウィンドウ、それに配置されたメニュー、コントロール等の個別のウィンドウインスタンスのスクリーン座標位置、領域(幅、高さ)、表示順(Zオーダー)を常時管理し、それに合わせて、

③「奥(Zオーダーの低い方)」から順に(現実にはオーバーラップ領域一番上の画像のみ)

描画してゆくことで入出力を管理します。

(4)画面描画は画面情報に変化がある都度、描画を行うウィンドウメッセージ(WM_PAINT)により描画処理され、常に最新の位置、領域、順序の情報が反映されます。

 

なにか、いつもPCが何も言わずに遂行している「GUIの中の基本中の基本の画像描画処理」を具体的に考えるだけで素人はお腹がいっぱいになってしまいますね。

 

うへぇ。

 

まぁ、素人はPCをブラックボックスとして扱うのが無難である、ということはわかると思います。次はもっと絞ってウィンドウの描画についてみてみますか。

 

ps. ウィンドウと言えば、昔Z80という8bitのROM Basic PC(具体的にはMZ2500といいます)でアセンブラーによるプログラミングをしていた時に「CUIでマルチウィンドウ」を実現したエディターを使っていました。勿論64KBの空間なので一つのファイルの異なる部分を2-3のウィンドウで表示するだけのものでしたが、↓のような表示でそれはそれで面白かったです。

 ーーーーーーーーーーー

|abcde        |

|01234        |

|           |ーーーーーーー

|           |       |

|           |       |

 ーーーーーーーーーーー        |

         |vwxyz       |

         |56789       |

          ーーーーーーーーーー

 

 

 

 

 

日本語、英語ともにChatGPTを試してみるためにアカウントを作って、昔の飯のタネ(仕事)であった米国訴訟やリスクマネジメントの話を聞いてみたら、「うん、なるほど」とうなるお答え。流石、Open AIだな、と思いました。

で、

今更ですが、「こっちが本職だろう」ということで、プログラミングについて質問してみました。

 

ということで、表題になった次第。(冗談無しで、今までGoogle+Microsoft LearnやDocを見ていたのですが、直接質問した方が早いかも。実際回答も丁寧だし。)

 

ps. ブログをアップしようとして、「ん??」ということでChatGPTに「C#とC++でウィンドウにビットマップ画像を表示するプログラムを書いてもらえますか?」と質問しました。その結果は...

【ChatGPTの回答】

はい、以下はC#とC++でウィンドウにビットマップ画像を表示するプログラムの例です。

C#の場合:

using System;

using System.Drawing;

using System.Windows.Forms;

class BitmapForm : Form

{

    private Bitmap image;

    public BitmapForm()

    {

        this.image = new Bitmap("image.bmp");

        this.ClientSize = new Size(this.image.Width, this.image.Height);

       this.Paint += new PaintEventHandler(this.BitmapForm_Paint);

    }

    private void BitmapForm_Paint(object sender, PaintEventArgs e)

    {

        e.Graphics.DrawImage(this.image, 0, 0);

    }

    static void Main()

    {

        Application.Run(new BitmapForm());

    }

}

この例では、BitmapFormクラスを定義し、コンストラクタで画像を読み込んでウィンドウサイズを設定します。また、Paintイベントでウィンドウを描画し、Mainメソッドでアプリケーションを実行しています。

 

C++の場合:

#include <Windows.h>

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

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

    // ウィンドウクラスの登録

    WNDCLASS wc = {};

    wc.lpfnWndProc = WindowProc;

    wc.hInstance = hInstance;

    wc.hCursor = LoadCursor(NULL, IDC_ARROW);

    wc.lpszClassName = L"MyWindowClass";

    RegisterClass(&wc);

    // ウィンドウの作成

    HWND hwnd = CreateWindowEx(0, L"MyWindowClass", L"Window", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL); ShowWindow(hwnd, nCmdShow);

    // 画像の読み込み

    HBITMAP hBitmap = (HBITMAP)LoadImage(NULL, L"image.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);

    // ウィンドウの描画 HDC hdc = GetDC(hwnd);

    HDC hdcMem = CreateCompatibleDC(hdc);

    SelectObject(hdcMem, hBitmap);

    BITMAP bitmap = {};

    GetObject(hBitmap, sizeof(BITMAP), &bitmap);

    BitBlt(hdc, 0, 0, bitmap.bmWidth, bitmap.bmHeight, hdcMem, 0, 0, SRCCOPY);

    DeleteDC(hdcMem);

    ReleaseDC(hwnd, hdc);

    // メッセージループの開始

    MSG msg = {};

    while (GetMessage(&msg, NULL, 0, 0)) {

        TranslateMessage(&msg);

        DispatchMessage(&msg);

    }

    return (int)msg.wParam;

}

 

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {

    switch (uMsg) {

        case WM_DESTROY: PostQuitMessage(0);

            return 0;

        default: return DefWindowProc(hwnd, uMsg, wParam, lParam);

    }

}

この例では、WindowProc関数を定義し、ウィンドウプロシ(注:文字数制限からちょん切れたようです。)

 

恐るべし、ChatGPT!

Windowsに関わる過去の振り返りは何度か書きました。(注)

 

要約すると、Microsoftが生まれ、MS-DOSで一山当て、IBMと袂を分かちOS/2を捨ててWindowsに専念し、インターネットの到来とともに1995年のWindows 95(Windows 4.0)でもう一山当てたのがピークで、その後スマートフォンに代表されるモバイルデバイスの台頭とともにPCコンピューティングが低迷し始め、Windows 10で従前の阿漕なOS商売に見切りをつけたのですが、ハードの進化から4GB・32bitの壁を越えなければならず、Windows 11で64bit版のみとなったのが現在、現状と言えるのではないでしょうか?

注:関連記事

 

 

 

 

私はBasic言語開発環境がOSであった8bit時代や、CUI(Character User Interface-文字入出力インターフェース)のMS-DOSであった16bit時代初めまでは、コンピューター資源と運営のマネージャーであるOS(Operating System)が

「シングルタスク(プロセス)、シングルスレッド」で「コンピューターの一つのCPUが一つの仕事(プログラム-プロセス、タスク)をしている間は他の仕事はできないし、仕事は順に進めてゆく(逐次処理)」のが当たり前

の環境でプログラミングを始めたので、プログラムの実行は「プログラムの頭から始まり、プログラムの最後で終わる」ものだと思っていました。

例:Start→「初期設定」→「初期画面表示」→「ユーザー入力」(ここでループが入る)→「入力内容の点検」(ここで分岐)→「入力データ処理」または「エラー処理」→(更に先へ進む...)→End

 

その為、1991年頃MS-DOSのTurbo C++では難なくプログラミングが出来たのに、Windows 3.0用のOWL(Object Windows Library)(注)を使うBorland C++が出てきた時にはプログラムを読んでも何がどうなっているのか分からずに悔しかったこと、その結果プログラミングをやめてしまったことを覚えています。

注:Wiki。なお、物好きな方がおればBorland C++ 3.0のOWLが手に入りますのでDLしてみてください。

 

その後、本格的な

GUIGraphical User Interface-画入出力インターフェース)で

「マルチタスク(プロセス)、マルチスレッド」(注)で「コンピューターの一つのCPU(現在は複数コアのチップが当たり前だが...)が、各仕事の処理時間(スレッド)を割り振りながら、複数の仕事(プロセス、タスク)を同時に処理を進めてゆく(同時処理)」のが当たり前

のWindows 95が出てきて、久々にActive Basicという開発環境でプログラミングを再学習して初めてどのようにWindowsでプログラミングをするのか、とか様々なWin32 APIを構造的に理解ができるようになりました。

例:Start→「アプリケーションウィンドウ登録」→「ウインドウ作成と表示」→「コールバック関数によるループ」→「ウィンドウ破棄」→「プロセス終了処理」→End(「コールバック関数によるループ」の中で、ユーザーがマウス、キーボードその他を使って行う様々な割込みでそれぞれの処理を行い、全体としての一つの仕事ををする。)

注:マルチタスクマルチスレッドといえば、例えば、Windows 11を立ち上げます。(昔と違ってSSDで一瞬ですよね。)セキュリティチェックを受けてデスクトップが現れます。そ入れからしてウィンドウ、メニューバーもウィンドウ、ポップアップメニューもウィンドウ、プログラムアイコンもウィンドウ(コントロール)で画面上ウィンドウで溢れています。プロセスはもっといっぱい動いており、Ctrl+Alt+Delでタスクマネージャーを呼び出すと「よくもまぁこんなにプログラムが同時に走っているな」という感慨にふけります。

 

「今は昔」を味わっていただくために、Embarcadero C++による「メニュー付きSDIウィンドウを表示するだけのスケルトン」を以下にご紹介します。(BatchGoodをつかってコンパイルできますよ。しかし、C#と対比するとまるで現代のアッセンブラーのようですね。)

 

【Resource.rc】

//-----------------------------------------
//             BCCForm Ver 2.41
//    An Easy Resource Editor for BCC
//  Copyright (c) February 2002 by ysama
//-----------------------------------------
#include    "Resource.h"

//-------------------------
// メニュー(IDM_MENU)
//-------------------------
IDM_MENU MENU DISCARDABLE
{
    POPUP "ファイル(&F)"
    {
        MENUITEM "新規(&N)", IDM_NEW
        MENUITEM "データを開く(&O)", IDM_OPEN
        MENUITEM "データの保存(&S)", IDM_SAVE
        MENUITEM "名前をつけて保存(&A)", IDM_SAVEAS
        MENUITEM SEPARATOR
        MENUITEM "終了(&X)", IDM_EXIT
    }
    POPUP "編集(&E)"
    {
        MENUITEM "切り取り(&T)", IDM_CUT
        MENUITEM "コピー(&C)", IDM_COPY
        MENUITEM "貼り付け(&P)", IDM_PASTE
    }
    POPUP "表示(&V)"
    {
        MENUITEM "標準(&S)", IDM_NORM
    }
    POPUP "ヘルプ(&H)"
    {
        MENUITEM "ヴァージョン情報(&V)", IDM_VERSION
    }

}

IDI_ICON ICON DISCARDABLE    "Icon.ico"
 

【Resource.h】

//-----------------------------------------
//             BCCForm Ver 2.41
//   Header File for Resource Script File
//   Copyright (c) February 2002 by ysama
//-----------------------------------------
//---------------------
//  ダイアログリソース
//---------------------

//---------------------
//  メニューリソース
//---------------------
// メニュー IDM_MENU
#define    IDM_NEW            100
#define    IDM_OPEN        101
#define    IDM_SAVE        102
#define    IDM_SAVEAS        103
#define    IDM_EXIT        104
#define    IDM_CUT            105
#define    IDM_COPY        106
#define    IDM_PASTE        107
#define    IDM_NORM        108
#define    IDM_VERSION        109

//---------------------
//  イメージリソース
//---------------------
#define    IDI_ICON        200

//---------------------
//  ストリングテーブル
//---------------------

//--------------------
//  アクセラレーター
//--------------------

//------------------
//  ヴァージョン情報
//------------------
 

【Header.h】

///////////////////////////
// Windows Program Skelton
///////////////////////////
#define        STRICT
#include    <windows.h>
#include    <string.h>
#include    "Resource.h"

//外部変数
char szClassName[] = "Windows_Class_Name";
char szTitleName[] = "SDI Windows";

【Procedures.h】

////////////////////////////////////
//            Procedure.h
//注:アプリケーションが処理した場合
//  TRUEを返すと、こーづバック関数
//  は0を返し、FALSEを返すとDef-
//  WindowProcを呼びます。
////////////////////////////////////
//メッセージ処理関数定義
////////////////////////
bool OnCreate(HWND hWnd) {


    //「アプリケーションがこのメッセージを処理する場合は、
    // ウィンドウの作成を続行するために 0 を返す必要があ
    // ります。アプリケーションから -1 が返された場合、
    // ウィンドウは破棄され」ます。(Microsoft DOC)
    //return TRUE;
    return FALSE;
}

bool OnLButtonDown() {

    //return TRUE;
    return FALSE;
}

bool OnPaint(HWND hWnd) {

/*
    PAINTSTRUCT paint;
    HDC hDC = BeginPaint(hWnd, &paint);

    EndPaint(hWnd, &paint);
    return TRUE;
*/
    return FALSE;
}
 

 

 

【SDI.h】

///////////////////////////////////////////
// Windows SKD Skelton - WinSKDSkelton.cpp
///////////////////////////////////////////

////////////////////////////
//ウィンドウコールバック関数
////////////////////////////
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {

    bool Done = FALSE;
    switch(msg){
        case WM_COMMAND:
            switch(LOWORD(wParam)) {
            case IDM_NEW:
            case IDM_OPEN:
            case IDM_SAVE:
            case IDM_SAVEAS:
            case IDM_CUT:
            case IDM_COPY:
            case IDM_PASTE:
            case IDM_NORM:
            case IDM_VERSION:
                MessageBox(hWnd, "ダミーです。", "メッセージ", MB_OK | MB_ICONINFORMATION);
                Done = TRUE;
                break;
            case IDM_EXIT:
                SendMessage(hWnd, WM_CLOSE, NULL, NULL);
                Done = TRUE;
                break;
            }
            break;
        case WM_CREATE:
            Done = OnCreate(hWnd);
            break;
        case WM_LBUTTONDOWN:
            Done = OnLButtonDown();
            break;
        case WM_PAINT:
            Done = OnPaint(hWnd);
            break;
        case WM_CLOSE:
            if(MessageBox(hWnd, 
                (LPCSTR)"終了してもよいですか", 
                (LPCSTR)"終了確認", 
                MB_YESNO | MB_ICONQUESTION) == IDYES) {
                DestroyWindow(hWnd);
            }
            Done = TRUE;
            break;
        case WM_DESTROY:
            PostQuitMessage(0);
            Done = TRUE;
            break;
        default:
            break;
    }
    if(Done)
        return 0L;
    else
        return(DefWindowProc(hWnd, msg, wParam, lParam));
}

///////////////////////////
//ウィンドウ・クラスの登録
///////////////////////////
BOOL InitApp(HINSTANCE hInstance) {

    WNDCLASSEX wc;
    wc.cbSize            = sizeof(WNDCLASSEX);                //この構造体のサイズ
    wc.style            = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc        = WndProc;        //プロシージャ名
    wc.cbClsExtra        = 0;
    wc.cbWndExtra        = 0;
    wc.hInstance        = hInstance;    //インスタンス
    wc.hIcon            =
    wc.hIconSm            = LoadIcon(hInstance, "IDI_ICON");    //最小化時の小さいアイコン
    wc.hCursor            = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground    = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName        = "IDM_MENU";    //メニュー名
    wc.lpszClassName    = (LPCSTR)szClassName;
    return (RegisterClassEx(&wc));
}

///////////////////
//ウィンドウの生成
///////////////////
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow){

    HWND hWnd = CreateWindowEx(
            NULL,                    //dwExStyle
            szClassName,            //ウィンドウクラス名
            szTitleName,            //タイトルバーにこの名前が表示されます
            WS_OVERLAPPEDWINDOW,    //ウィンドウの種類
            CW_USEDEFAULT,            //X座標
            CW_USEDEFAULT,            //Y座標
            CW_USEDEFAULT,            //幅
            CW_USEDEFAULT,            //高さ
            NULL,                    //親ウィンドウのハンドル、親を作るときはNULL
            NULL,                    //メニューハンドル、クラスメニューを使うときはNULL
            hInstance,                //インスタンスハンドル
            NULL);
    if(!hWnd)
        return FALSE;
    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);
    return TRUE;
}

WPARAM Loop(HINSTANCE hCurInst, HINSTANCE hPrevInst, LPSTR lpszCmdLine, int nCmdShow) {

    MSG msg;
    if(!hPrevInst) {
        if(!InitApp(hCurInst))
            return FALSE;
    }
    if(!InitInstance(hCurInst, nCmdShow)) {
        return FALSE;
    }
    while(GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return msg.wParam;
}
 

【Program.cpp】

/////////////////////////////
//        Program.cpp
/////////////////////////////
//////////////////////
//インクルードファイル
//////////////////////
#include    "Header.h"

//外部変数

//ウィンドウ割り込み処理関数
#include    "Procedures.h"
//メインウィンドウ登録、生成、メッセージ処理
#include    "SDI.h"

////////////////////////
//ウィンドウズメイン関数
////////////////////////
int WINAPI WinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst, LPSTR lpszCmdLine, int nCmdShow) {

    return Loop(hCurInst, hPrevInst, lpszCmdLine, nCmdShow);
}
 

【Program.bat】

@ECHO OFF
ECHO ----------------------------------
ECHO  BatchGood - Batch File Generator
ECHO  for Embarcadero "bcc32c.exe"
ECHO  Copyright (c) 2021 by Ysama
ECHO ----------------------------------

cd "C:\Users\(省略)\Windows Program\WinTemplate\Debug"
"C:\Borland\BCC102\bin\bcc32c.exe" "C:\Users\(省略)\Windows Program\WinTemplate\Program.cpp" -tW -w-
del "C:\Users\(省略)\Windows Program\WinTemplate\Debug\Program.tds"
"C:\Borland\BCC102\bin\brc32.exe" "C:\Users\(省略)\Windows Program\WinTemplate\Resource.rc" "C:\Users\(省略)\Windows Program\WinTemplate\Debug\Program.exe"
del "C:\Users\(省略)\Windows Program\WinTemplate\Resource.res"
pause

 

やっと腰痛症状が緩和され、未だ若干の痛みと違和感がありますが、しっかり歩くことが出来るようになりましたので、ブログを再開させていただきます。

 

前に書いたように、腰痛との付き合いは長く、筋肉系ぎっくり腰、神経系椎間板ヘルニア(MRI検査で確認できます)が周期的に起きるので今回もその前(2021年7月)から2年、その前の前(2018年正月)から5年経つので、まぁそんなものかの類ではありますが、ものを考えるにはよい機会だとも言えます。ー追記:2022年1月にもやってますね...汗;

 

本ブログは2020年に定年退職し、またプログラミングを始めて、一応再学習を行った2021年5月14日から始めました。内容はBorland(現Embarcadero)フリーC++コンパイラーの開発ツールに関わる解説やプログラミング上のTips等を目的にしていたのですが、どうも読んでいただける方がプログラミングとは余り関係のなさそうな(特に-嬉しいことに!)女性の方が多く、また実際ブログを始めてから2年間でBCCForm and BCCSkeltonをダウンロードしていただいた方はたったの10名(!!!)で、アクセス数の多い回は(逆にBCCForm and BCCSkeltonと関係のない)無駄話やC#でのプログラミングだったりするので、「いてててて」と思いながら、本ブログの3年目以降の構想を考えてみました。

 

その結果、(ブログのタイトルに関わらず)BCCForm and BCCSkelton(含むECCSkelton)に余り拘らず、自由に思いついたテーマや遭遇した問題について読者層と関心を意識して書いてゆくことにします。

 

さて、では当面のテーマですが、最近のネタ探しの結果「昔のBasicプログラムをC#に焼き直しては?」ということで「高級言語」のC#での(基本GDIとGDI+を使った-注)描画について調べてみました。

注:Graphic Device Interfaceの略で、要すればグラフィック描画系のWindows API(Application Program Interface-今は64bitの登場でWin32 APIと言わなくなったらしいです)で、更に要すればWIndowsというOSやMicrosoftが用意する描画する際に利用できる処理サービス(用役の意味です)のことです。

 

しかし、ウィンドウズの描画は相変わらず結構面倒くさいことになっているので「なんでそうなるのか」という点を「下級言語」の視点から少し説明を行い、より簡単に描画できるツールクラスを作ってみようか、と考えています。

 

私は25歳の時に腰を痛め、以降腰痛体操を継続し、ここ20年は毎朝起きがてらにストレッチと体幹筋トレも行ってきましたが、不覚にもまたやってしまいました。(注)

注:腰痛との付き合いも今年で43年になるので、それが筋肉系のぎっくり腰か、(私の場合)右坐骨神経を襲うヘルニアによる神経系かわかるようになっていますが、今回はどういうわけか左側筋肉(側腹筋)系の痛みのようです。

 

上体を起こすような起き上がる動作で痛みが出て力が入らないので、結構不自由な日常生活を強いられ、少しの間更新をお休みさせていただきます。

 

ごめんなさい。

 

今までMicrosoft C#の利用可能なコンポーネントオブジェクトとしての音声合成(注)については、COMコンポーネントのSAPI(5.3)、OneCoreの音源も使えるWin/RT等を併せて色々とみてみましたが、C#のサンプルを末尾に(私の環境のコンパイルオプションと共に)載せておきます。

注:System.Speech.dllに入っている、System.Speech.Synthesis.SpeechSynthesizerクラス。

 

BCCForm and BCCSkeltonのサンプルではないので詳述しませんが、フォームにアンカー付きでTextBoxとButton5つを貼り付け、手入力したまたはファイルから「読み込(み)」んだTextBoxのテキスト(プレーンテキスト、SSMLいずれでも結構です)を「読み上げ」ます。気に喰わなければ「文章消去」できます。

 

このサンプルで利用可能な音源は、SpeechSynthesizer.GetInstalledVoices()を使って「詳細」を見ることが出来、この中で使いたいものがあれば「音声選択」が可能です。(「詳細」と合わせてリストビューが相当ですが、手を抜いてMessageBoxを繰り返します。)

 

音声合成がらみで見ておくとよい所は赤字にしておきました。ご参考になれば幸いです。

 

なお、コマンドラインのC++(旧bcc32.exe)でSAPI 5.3のCOMコントロールを使いたくて、Microsoft Learnをはじめ、多くの記事を読んでCOM、Unicode、カルチャ等を学び、OneCoreの音源も使いたくてレジストリー操作も調べていた時に比べると、コンポーネント化されたSpeechSynthesizerは本当に簡単に処理ができるんだなぁ、という感慨を受けました。

 

しかしっ、

 

更に上手がいるもので、あとで(最新のブラウザーとHTMLの)これとか、あれとか見るにつき、なんかウィンドウを作ってプログラムを書くのが馬鹿らしくなりました。(ブラウザーだと、ユーザーがインストールしていなくてもすべての音源のリストが出る点に注意してください。)

 

【Text2Speech.cs】

//////////////////
// Text2Speech.cs
//////////////////
using System;
using System.Windows.Forms;
using System.Drawing;        //Size等を使う為
using System.IO;            //StreamReaderを使う為
using System.Text;            //Encodingを使う為
using System.Speech.Synthesis;
using System.Speech.AudioFormat;

//コンパイル時に"/reference:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Speech\v4.0_4.0.0.0__31bf3856ad364e35\System.Speech.dll"オプションが必要

//昔はSpeechLib.dllがあったようだ
//https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ms720177(v=vs.85)
//https://www.valuestar.work/news/archives/44
//https://petile.com/mahoroba/e1590.html
//using SpeechLib;
//SpVoice sp = new SpeechLib.SpVoice();
//sp.Speak("テスト");


namespace Text2Speech
{
    public partial class AppForm : Form
    {
        //フォームのコントロール
        TextBox txtBox;
        Button clrBtn, rdBtn, spBtn, sltBtn, swBtn, extBtn;
        //スピーチシンセサイザー
        SpeechSynthesizer synth;


        [STAThread]
        public static void Main()
        {
            AppForm ap = new AppForm();
            Application.Run(ap);
        }

        public AppForm()
        {
            this.Size = new Size(640, 480);
            this.MinimumSize = new Size(320, 260);
            this.Text = "System.Speech Test";
            this.Load += AppForm_Load;
        }

        private void AppForm_Load(object sender, EventArgs e)
        {
            //Clearボタン
            clrBtn = new Button();
            clrBtn.Location = new Point(ClientSize.Width - clrBtn.Width - 10, 10);
            clrBtn.Text = "文章消去";
            clrBtn.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            clrBtn.Click += Button1_Click;
            this.Controls.Add(clrBtn);

            //Readボタン
            rdBtn = new Button();
            rdBtn.Location = new Point(ClientSize.Width - rdBtn.Width - 10, clrBtn.Height + 20);
            rdBtn.Text = "読み込み";
            rdBtn.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            rdBtn.Click += Button2_Click;
            this.Controls.Add(rdBtn);

            //Speechボタン
            spBtn = new Button();
            spBtn.Location = new Point(ClientSize.Width - spBtn.Width - 10, clrBtn.Height + rdBtn.Height + 30);
            spBtn.Text = "読み上げ";
            spBtn.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            spBtn.Click += Button3_Click;
            this.Controls.Add(spBtn);

            //Selectボタン
            sltBtn = new Button();
            sltBtn.Location = new Point(ClientSize.Width - sltBtn.Width - 10, clrBtn.Height + rdBtn.Height + spBtn.Height  + 40);
            sltBtn.Text = "音声選択";
            sltBtn.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            sltBtn.Click += Button4_Click;
            this.Controls.Add(sltBtn);

            //Showボタン
            swBtn = new Button();
            swBtn.Location = new Point(ClientSize.Width - swBtn.Width - 10, clrBtn.Height + rdBtn.Height + spBtn.Height + sltBtn.Height + 50);
            swBtn.Text = "詳細";
            swBtn.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            swBtn.Click += Button5_Click;
            this.Controls.Add(swBtn);

            //Exitボタン
            extBtn = new Button();
            extBtn.Location = new Point(ClientSize.Width - extBtn.Width - 10, ClientSize.Height - extBtn.Height - 20);
            extBtn.Text = "終了";
            extBtn.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right);
            extBtn.Click += Button6_Click;
            this.Controls.Add(extBtn);

            //テキストボックス
            txtBox = new TextBox();
            //位置
            txtBox.Location = new Point(10, 10);
            txtBox.Anchor = (AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right);
            //サイズ
            txtBox.Width = ClientSize.Width - clrBtn.Width - 30;
            txtBox.Height = ClientSize.Height - 20;
            //その他プロパティ
            txtBox.Multiline = true;
            txtBox.ScrollBars = ScrollBars.Both;
            txtBox.WordWrap= true;
            txtBox.Font = new Font(txtBox.Font.OriginalFontName, 12f);    //フォントサイズを変更する
            //FormにTextBoxを追加
            this.Controls.Add(txtBox);

            //スピーチシンセサイザーの初期化
            try
            {
                //音声合成インスタンスの作成
                synth = new SpeechSynthesizer();
                //音声出力をデフォルトデバイスにする
                synth.SetOutputToDefaultAudioDevice();
            }
            catch
            {
                MessageBox.Show("スピーチシンセサイザーが使えません。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                //終了処理
                Close();
            }

        }

        //終了処理
        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            base.OnFormClosing(e);
            DialogResult dr = MessageBox.Show("終了しますか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
            if(dr == DialogResult.No)
            {
                e.Cancel = true;
            }
            else
            {
                //現在ペンディングの音声キューをキャンセルする
                synth.SpeakAsyncCancelAll();
                //リソースの開放
                synth.Dispose();

            }
        }

        private void Button1_Click(object sender, EventArgs e)
        {
            //txtBoxをクリアする
            txtBox.Clear();
        }

        private void Button2_Click(object sender, EventArgs e)
        {
            //txtBoxにファイルを読む
            OpenFileDialog ofDlg = new OpenFileDialog();
            ofDlg.FileName = "*.*";                        //初期ファイル名の指定
            ofDlg.Filter = "すべてのファイル(*.*)|*.*";    //ファイルフィルターの指定
            ofDlg.FilterIndex = 1;                        //ファイルフィルターインデックスの指定
            ofDlg.RestoreDirectory = true;                //ダイアログボックスを閉じる前に現在のディレクトリを復元するようにする
            ofDlg.CheckFileExists = true;                //存在しないファイルの名前が指定されたとき警告を表示する
            ofDlg.CheckPathExists = true;                //存在しないパスが指定されたとき警告を表示する
            ofDlg.Multiselect = true;                    //複数ファイルの選択を行う
            ofDlg.InitialDirectory = @".\";                // デフォルトのフォルダを指定する
            ofDlg.Title = "ファイルを開く";                //ダイアログのタイトルを指定する
            if(ofDlg.ShowDialog() == DialogResult.OK)    //ダイアログを表示する
            {
                //StreamReaderでtxtBoxに"Shift_JIS"ファイルを読み込む
                StreamReader sr = new StreamReader(ofDlg.FileName, Encoding.GetEncoding("Shift_JIS"));
                 txtBox.Text = sr.ReadToEnd();
                sr.Close();
            }
            else
            {
                MessageBox.Show("キャンセルされました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            // オブジェクトを破棄する
            ofDlg.Dispose();
        }

        private void Button3_Click(object sender, EventArgs e)
        {
            DialogResult dr;    //MessageBoxの回答
            //音声の状況を確認する
            if(synth.State != SynthesizerState.Ready)    //0 - SynthesizerState.Ready、1 - SynthesizerState.Speaking、2 - SynthesizerState.Paused
            {
                string str = (synth.State == SynthesizerState.Speaking) ? "読み上げ中です。中断しますか?" : "読み上げ停止中です。中断しますか?";
                dr = MessageBox.Show(str, "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
                if(dr == DialogResult.Yes)
                {
                    //すべてのペンディング読み上げを中断
                    synth.SpeakAsyncCancelAll();
                    //Readyになるまで~ループを回す
                    while(synth.State != SynthesizerState.Ready);
                }
                else
                    return;        //「読み上げ」をキャンセル
            }
            //音量設定
            synth.Volume = 50;    //0 - 100で設定
            //音速設定
            synth.Rate = 0;    //-10 - 10で設定
            //同期、非同期の選択
            dr = MessageBox.Show("同期で読みますか?(はい)、\r\n非同期にしますか?(いいえ)", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
            if(dr == DialogResult.Yes)
            {
                dr = MessageBox.Show("SSMLで読みますか?(はい)、\r\nTextにしますか?(いいえ)", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
                if(dr == DialogResult.Yes)
                    //SSMLテキストによる読み上げ
                    synth.SpeakSsml(txtBox.Text);    //SSML同期読み上げ-中断できない
                else
                    //テキスト読み上げ
                    synth.Speak(txtBox.Text);        //同期読み上げ-中断できない
            }
            else
            {
                dr = MessageBox.Show("SSMLで読みますか?(はい)、\r\nTextにしますか?(いいえ)", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
                if(dr == DialogResult.Yes)
                {
                    //SSMLテキストによる読み上げ
                    synth.SpeakSsmlAsync(txtBox.Text);    //非同期読み上げ-中断できる
                    //SSMLテキストのPrompt読み上げ
                    //Prompt pmt= new Prompt(txtBox.Text);
                    //synth.SpeakAsync(pmt);            //非同期読み上げ-中断できる
                }
                else
                    //テキストによる読み上げ
                    synth.SpeakAsync(txtBox.Text);    //非同期読み上げ-中断できる
            }

        }

        private void Button4_Click(object sender, EventArgs e)
        {
            //インストールされた音声の列挙
            foreach (InstalledVoice voice in synth.GetInstalledVoices())
            {
                string Voice = "";
                VoiceInfo vinfo = voice.VoiceInfo;
                //音声様式情報をテキストボックスに表示
                Voice += "Name:    " + vinfo.Name + "\r\n";
                Voice += "Culture:    " + vinfo.Culture + "\r\n";
                Voice += "Age:    " + vinfo.Age + "\r\n";
                Voice += "Gender:    " + vinfo.Gender + "\r\n";
                DialogResult dr = MessageBox.Show(Voice + "この音声でよろしいですか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
                if(dr == DialogResult.Yes)
                {
                    synth.SelectVoice(vinfo.Name);        //音声の設定
                    break;;
                }
            }

        }

        private void Button5_Click(object sender, EventArgs e)
        {
            string buff = txtBox.Text;    //現在のテキストを退避させる
            txtBox.Clear();                //現在のテキストを消去
            //インストールされた音声の列挙(現在の音声は synth.Voice プロパティで取得できる)
            foreach (InstalledVoice voice in synth.GetInstalledVoices())
            {
                string Voice = "";                        //音声源情報(SelectVoice(String) が使える)
                VoiceInfo vinfo = voice.VoiceInfo;
                //音声様式情報をテキストボックスに表示
                Voice += "Name:        " + vinfo.Name + "\r\n";
                Voice += "Culture:        " + vinfo.Culture + "\r\n";
                Voice += "Age:        " + vinfo.Age + "\r\n";
                Voice += "Gender:        " + vinfo.Gender + "\r\n";
                Voice += "Description:    " + vinfo.Description + "\r\n";
                Voice += "ID:        " + vinfo.Id + "\r\n";
                Voice += "Enabled:        " + voice.Enabled + "\r\n";
                txtBox.Text += "<音声>\r\n" + Voice;
                //音声フォーマット(Audio Format)を調べる
                string AudioFormats = "<音声フォーマット>\r\n";    //Voice.SupportedAudioFormats-音声がサポートするオーディオ形式のコレクション
                foreach (SpeechAudioFormatInfo fmt in vinfo.SupportedAudioFormats)
                {
                    AudioFormats += String.Format("{0}\r\n", fmt.EncodingFormat.ToString());
                }
                if(vinfo.SupportedAudioFormats.Count != 0)
                {
                    txtBox.Text += AudioFormats + "\r\n";
                }
                else
                {
                    txtBox.Text += "<音声フォーマット>\r\n利用できる音声フォーマットがありません。" + "\r\n";
                }
                //その他情報を得る
                string AdditionalInfo = "<その他情報>\r\n";
                foreach (string key in vinfo.AdditionalInfo.Keys)
                {
                    AdditionalInfo += String.Format("{0}: {1}\n", key, vinfo.AdditionalInfo[key]);
                    }
                txtBox.Text += AdditionalInfo + "\r\n\r\n";
            }

            DialogResult dr = MessageBox.Show("テキストを復元しますか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
            if(dr == DialogResult.Yes)
            {
                txtBox.Text = buff;
            }
        }

        private void Button6_Click(object sender, EventArgs e)
        {
            //終了処理
            Close();
        }
    }
}
 

【Text2Speech.opt】

[Compile Option]
Target=2
Resource=0
RscFile=
IconFile=C:\Users\ysama\Programing\C# Programing\Samples\System_Speech\Text2Speech\Icon.ico
DbgOpt=0
WarnErr=5
Others=
RefFile=C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Speech\v4.0_4.0.0.0__31bf3856ad364e35\System.Speech.dll

 

C#のSystem.Speech.dllを使ったサンプルを紹介する前に、一つ話しておかなければならないと思われること(one thing that I think I've got to tell you)があります。

 

それは「どうもC#のSystem.Speech.Synthesizerの入力で使うマークアップ言語(以下SSML)は、(XMLをベースにしているという点は同じでも)今までのSAPIのマークアップ言語(SAPI-ML)とは異なると考えられる」ことです。

 

私が前に作ったTextToSpeechはSAPI 5.3のもので、マークアップ言語は有り体に言って簡易でわかりやすいものでした。(TextToSpeechのHelpファイルの付録参照)それはMicrosoft JavaやVisual Basic Scriptでも同じでした。

 

【Microsoft Java】

var tts = WScript.CreateObject("SAPI.SpVoice");
// しゃべる
tts.Speak("<lang langid='409'><rate speed='0'><pitch middle='0'>Hello world! Konnichiwa!</pitch></rate></lang>");
tts.Speak("<lang langid='409'><rate speed='5'><pitch middle='-0'>I think that that that that that boy wrote is wrong.</pitch></rate></lang>"); // 英語の早口言葉
tts.Speak("<lang langid='411'><rate speed='0'><pitch middle='10'>こんにちは!</pitch></rate></lang>");
tts.Speak("<lang langid='411'><rate speed='5'><pitch middle='0'>生麦、生米、生卵</pitch></rate></lang>"); // 日本語の早口言葉
tts.Speak("<lang langid='409'><rate speed='3'><gender='Male'><pitch middle='-10'>I'm not interested in Google.</pitch></Gender></rate></lang>");

 

【Visual Basic Script】

Dim sapi, voice
Set sapi = CreateObject("SAPI.SpVoice")
For Each voice In sapi.GetVoices
    WScript.Echo voice.GetDescription
Next

Set voice = sapi.GetVoices("Language=411", "Gender=Female")
Set sapi.Voice = voice(0)
sapi.Rate = 0            '-10~10の範囲で指定
sapi.Volume = 50            '0~100の範囲で指定
'読み上げる
sapi.Speak("<lang langid='411'><rate speed='0'><pitch middle='10'><volume level='100'>こんにちは!これはテストです!</volume></pitch></rate></lang>")


Set voice = Nothing
Set sapi = Nothing

 

しかしSSMLはこのSAPIーMLとは異なり、↑のSpeak()関数で使っているタグをつけた文字列を与えても応答しない(SSML)か、文字列として素直にそれを読み上げます(Text)。

 

最初は何か見落としたり、使い方を誤っているのかなと考え、WEBで調べましたが、どうもそういう(マークアップ言語の仕様が異なる)ことのようです。

 

因みにSSMLでの表記サンプルは次のようになります。

 

【SSMLサンプル1】

<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US">
    <voice name="en-US-JennyNeural">
        Good morning!
    </voice>
    <voice name="en-US-ChristopherNeural">
        Good morning to you too Jenny!
    </voice>
</speak>

 

【SSMLサンプル2】

<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="jp-JP">
今日は2009年1月29日です。今日は、よい天気です。
</speak>

 

C#のSynthesizerのSpeak関連メソッドでは文字列、SSML、Promptによる入力を受け付けるので、サンプルでは「同期、非同期」の違いを含め、色々なテストが行えるようにしておきます。

 

ps. 10年ほど前はC#でもSpeeckLib(.dll)を使うことでSAPI 5を使っていたようですが、今はSystem.Speech.dllを使うことが賢明なんでしょう。

【C#サンプルプログラムから抜粋】

//昔はSpeechLib.dllがあったようだ
//https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ms720177(v=vs.85)
//https://www.valuestar.work/news/archives/44
//https://petile.com/mahoroba/e1590.html
//using SpeechLib;
//SpVoice sp = new SpeechLib.SpVoice();
//sp.Speak("テスト");

 

昨日のブログで、

 

Windows.Media.SpeechSynthesis Namespace

なんじゃ、これ???

 

と書きました。

矢張り気になったのでこの英文サイトの概要を見て、SpeechSynthesizerクラスを見るとSAPIのTTSのようです。この名前空間に関連するGitHubにあるサンプルのうち、Speechメソッド関連を除いてみると末尾にある通りで、(Visual Studio用のコードですが)赤字で書いた"using Windows..."系のDLLは(Microsoft.NETフォールダーをいくら調べても)私の環境になく、Visual Studioで提供されるライブラリーが無くては使えないことが分かりました。まぁ、これは無視、ですね。

 

また、System.Speech.dllでSynthesizerクラスのVoiceプロパティに選択される、音声音源のコレクションは、HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices

にあるHaruka、Zira、Davidの3名で、

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens

にあるその他の方の音声音源は使えないようです。(これに関わる詳細はBCCForm and BCCSkeltonのサンプル、"TextToSpeech"のヘルプファイルの「付録」を参照してください。)

 

そんなこんなでウロウロしていたら、昔読んだ記事で目からうろこでした。成程、WinRTでしたか。更に調べてみるとこんな話も。そうか!System32フォールダーか!ということでWindows.Media.SpeechSynthesis.dllを探したのですが見当たりません。Windows.Media.Speech.dllはあるので、この中にひょっとして(望み薄...)あればと昔読んだ記事のプログラムをコンパイルしてみると、

 

fatal error CS0009: 'c:\Windows\System32\Windows.Media.Speech.dll' メタデータ ファイルを開けませんでした -- '間違ったフォーマットのプログラムを読み込もうとしました。 '

 

というエラーが出て力尽きました。まぁ、私は"System.Speech"で細々と凌いでゆきましょう。

 

【Scenario_SynthesizeText.xaml.cs】

//*********************************************************
//
// Copyright (c) Microsoft. All rights reserved.
// This code is licensed under the MIT License (MIT).
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

using System;
using System.Linq;
using Windows.ApplicationModel.Resources.Core;
using Windows.Globalization;
using Windows.Media.SpeechSynthesis;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;


namespace SDKTemplate
{
    public sealed partial class Scenario_SynthesizeText : Page
    {
        private SpeechSynthesizer synthesizer;
        private ResourceContext speechContext;
        private ResourceMap speechResourceMap;

        public Scenario_SynthesizeText()
        {
            InitializeComponent();
            synthesizer = new SpeechSynthesizer();
            
            speechContext = ResourceContext.GetForCurrentView();
            speechContext.Languages = new string[] { SpeechSynthesizer.DefaultVoice.Language };

            speechResourceMap = ResourceManager.Current.MainResourceMap.GetSubtree("LocalizationTTSResources");

            InitializeListboxVoiceChooser();
        }

        /// <summary>
        /// This is invoked when the user clicks on the speak/stop button.
        /// </summary>
        /// <param name="sender">Button that triggered this event</param>
        /// <param name="e">State information about the routed event</param>
        private async void Speak_Click(object sender, RoutedEventArgs e)
        {
            // If the media is playing, the user has pressed the button to stop the playback.
            if (media.CurrentState == MediaElementState.Playing)
            {
                media.Stop();
                btnSpeak.Content = "Speak";
            }
            else
            {
                string text = textToSynthesize.Text;
                if (!String.IsNullOrEmpty(text))
                {
                    // Change the button label. You could also just disable the button if you don't want any user control.
                    btnSpeak.Content = "Stop";

                    try
                    {
                        // Create a stream from the text. This will be played using a media element.
                        SpeechSynthesisStream synthesisStream = await synthesizer.SynthesizeTextToStreamAsync(text);

                        // Set the source and start playing the synthesized audio stream.
                        media.AutoPlay = true;
                        media.SetSource(synthesisStream, synthesisStream.ContentType);
                        media.Play();
                    }
                    catch (System.IO.FileNotFoundException)
                    {
                        // If media player components are unavailable, (eg, using a N SKU of windows), we won't
                        // be able to start media playback. Handle this gracefully
                        btnSpeak.Content = "Speak";
                        btnSpeak.IsEnabled = false;
                        textToSynthesize.IsEnabled = false;
                        listboxVoiceChooser.IsEnabled = false;
                        var messageDialog = new Windows.UI.Popups.MessageDialog("Media player components unavailable");
                        await messageDialog.ShowAsync();
                    }
                    catch (Exception)
                    {
                        // If the text is unable to be synthesized, throw an error message to the user.
                        btnSpeak.Content = "Speak";
                        media.AutoPlay = false;
                        var messageDialog = new Windows.UI.Popups.MessageDialog("Unable to synthesize text");
                        await messageDialog.ShowAsync();
                    }
                }
            }
        }

        /// <summary>
        /// This is invoked when the stream is finished playing.
        /// </summary>
        /// <remarks>
        /// In this case, we're changing the button label based on the state.
        /// </remarks>
        /// <param name="sender">unused object parameter</param>
        /// <param name="e">unused event parameter</param>
        void media_MediaEnded(object sender, RoutedEventArgs e)
        {
            btnSpeak.Content = "Speak";
        }

        /// <summary>
        /// This creates items out of the system installed voices. The voices are then displayed in a listbox.
        /// This allows the user to change the voice of the synthesizer in your app based on their preference.
        /// </summary>
        private void InitializeListboxVoiceChooser()
        {
            // Get all of the installed voices.
            var voices = SpeechSynthesizer.AllVoices;

            // Get the currently selected voice.
            VoiceInformation currentVoice = synthesizer.Voice;

            foreach (VoiceInformation voice in voices.OrderBy(p=>p.Language))
            {
                ComboBoxItem item = new ComboBoxItem();
                item.Name = voice.DisplayName;
                item.Tag = voice;
                item.Content = voice.DisplayName + " (Language: " + voice.Language + ")";
                listboxVoiceChooser.Items.Add(item);

                // Check to see if we're looking at the current voice and set it as selected in the listbox.
                if (currentVoice.Id == voice.Id)
                {
                    item.IsSelected = true;
                    listboxVoiceChooser.SelectedItem = item;
                }
            }
        }

        /// <summary>
        /// This is called when the user has selects a voice from the drop down.
        /// </summary>
        /// <param name="sender">unused object parameter</param>
        /// <param name="e">unused event parameter</param>
        private void ListboxVoiceChooser_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            ComboBoxItem item = (ComboBoxItem)(listboxVoiceChooser.SelectedItem);
            VoiceInformation voice = (VoiceInformation)(item.Tag);
            synthesizer.Voice = voice;

            // update UI text to be an appropriate default translation.
            speechContext.Languages = new string[] { voice.Language };
            textToSynthesize.Text = speechResourceMap.GetValue("SynthesizeTextDefaultText", speechContext).ValueAsString;
        }
    }
}

 

さて、折角MSCompAssをアップグレードしたので、DLL参照を行うようなサンプルネタがないかな、と考え、前にDLL参照を行った「音声認識」のサンプル(注)を思い出しました。

注:以下のシリーズ記事で、Visual Studio C#のプログラムが(1)と(2)、MSCompAss用は(3)に書いています。

【C#】Visual Studioのファイルを調べてみる(1)

【C#】Visual Studioのファイルを調べてみる(2)

【C#】Visual Studioのファイルを調べてみる(3完)

 

これは自分で企画して書いたのではなく、MSChartでちょっと悪戯した後、「似たような面白いオブジェクトはないか?」とWebを漁っていて偶然取得したVisual Studioサンプルが音声認識を扱っていたのでそれを扱ったのでした。その中にあったコードが、

 

using System.Speech.Recognition;”

 

また、それを使う為にこのパイラーが参照したDLLが"System.Speech.dll" ...

 

ん?

 

思い返せば大分前、(既に昔、BCCForm and BCCSkeltonでWin32 APIベースのSAPIというCOMを使って"TextToSpeech"という音声による読み上げプログラムを作っていたので)"Text to Speech(TTS)"というキーワードを使って色々とググったら、GoogleのTTSエンジンの話しか出なかったこと、またその後見つけたC#のサンプルはテキスト読み上げをする為にWin32 APIベースのSAPIを使うという「面倒臭い」ものだったことからネタとして不採用したことを思い出しました。

 

しかし、

"System.Speech.dll"って「音声読み上げ用のクラスもあるんじゃない?」と思い、今度は"System.Speech"をキーワードにして調べると、あった、あった、ありましたよ。

 

System.Speech.Synthesizer クラス

およびそれが含まれる

System.Speech.Synthesis 名前空間

 

こいつらで、MSChartのように又簡単に音声出力ができるんだろうと考え、早速サンプルを作ってテストして確認しました。

次はこれで時間稼ぎしようと思っています。

 

が、

この空間は広く、未知のものが多く転がっているので、少しお勉強をした方が良いようだな、と調べているうちに、

 

Windows.Media.SpeechSynthesis Namespace

 
なんていう似て非なるものも出てきた。
 
なんじゃ、これ???

 

「二人はどんな関係?」と無粋な質問をしたくなる。どうもこれはいわゆる"UWP (Universal Windows Platform)"という世界の住人らしく、System.Speechが.NET APIの世界なので「異世界の似た者同士」なのかしら?(注)

注:MicrosoftがWindowsのプラットフォームを更新する度に、新しいアブリビーションが増えて混乱します。少し整理しましょう。

Microsoft .NER Framework(Win 7から)

UWP(Win 10から-こっちの方が分かりやすいか?ご本家のこれは殆ど「寿限無、寿限無」の世界だな。)

 

まだわかりづらいので全体像がないかと思って調べると、こんなのがあった。(正しいか、どうかは知らんけど...まぁ、その道の人も苦労するぐらいに分かりづらい、ということはわかります。)

 

At the end of the day,

 

もう少し勉強してサンプルプログラムを作りますわ。

 

 

 

Windows 10や11に標準でついてくるC#、VBコンパイラー(csc.exe、vbc.exe)を「昔のROM Basic感覚で使う」、"Microsoft Compiler Assistant"というコンセプトでBCCForm and BCCSkeltonで作ったMSCompAssですが、最初は(Microsoft C#では非常に大切な)外部DLL参照オプション("/reference:"または"/r:")を「無知の為」に入れていませんでした。

 

その後、エントリーポイントのないアセンブリーが簡単にDLLになり、参照するだけで簡単に使えることを知ったので、Version 1.3からDLL参照("/reference:"または"/r:")をオプションダイアログに加えました。

この段階ではDLL参照は(ダイアログのエディットボックスに)「一つだけ」できるだけでした。

 

所が、MSChartのサンプルを書いた際に、ソースファイルにいくら

"//MS Chart利用の為追加される宣言
 using System.Windows.Forms.DataVisualization.Charting;”

と書いても、コンパイラー(csc.exe)には伝わらず、Explorerで調べたファイルパス(注)を"/reference:"で追加してやらないとコンパイルできないことを知りました。

:私の環境では↓の通り。Visual StudioのIDEでは、MSChartを使う操作をするとIDEが自動的にDLL参照を追加してくれる。

"C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Windows.Forms.DataVisualization\v4.0_4.0.0.0__31bf3856ad364e35\System.Windows.Forms.DataVisualization.dll"

 

「これはひょっとして、一つじゃ足りないかな?」

 

ということで、参照先を増やした方がよいのではないか?と考え始めましたが、その時は(ダイアログデザインをやり直して、コードも書き直してと結構手間がかかるので面倒に思い)「いざとなれば『その他オプション』でも追加できるか」と考えていました。(今振り返れば、現実にはタイプ量が多いので無理かと...汗;)

 

で、

 

今般やっと重い腰を上げて書き直し始めたのですが、結構ムズイ感じの所もあり、またVersion 1.4で気になっていたところや、見直しで判明した(潜在的)バグも発見して、まとめて修正しました。

 

所で、今般のプログラム修正にかかわる調査で「csc.exeが"using"ステートメントで使う一般的なDLLをどのように参照しているか」について初めて知りました。また、今までは、';'をセパレーターにして"/reference:<DLL1パス>;<DLL2パス>;<DLL3パス>"のように表記するものと考えていたDLL参照の表記方法が「"/reference:"は一つだけのDLLの参照しかできない」ことを初めて知りました。(

:csc.exeはcsc.rspという参照先一覧ファイルを読み込んでコンパイルするようです。因みに私の環境では次の通り。(ご参考

 

# This file contains command-line options that the C#
# command line compiler (CSC) will process as part
# of every compilation, unless the "/noconfig" option
# is specified. 

# Reference the common Framework libraries
/r:Accessibility.dll
/r:Microsoft.CSharp.dll
/r:System.Configuration.dll
/r:System.Configuration.Install.dll
/r:System.Core.dll
/r:System.Data.dll
/r:System.Data.DataSetExtensions.dll
/r:System.Data.Linq.dll
/r:System.Data.OracleClient.dll
/r:System.Deployment.dll
/r:System.Design.dll
/r:System.DirectoryServices.dll
/r:System.dll
/r:System.Drawing.Design.dll
/r:System.Drawing.dll
/r:System.EnterpriseServices.dll
/r:System.Management.dll
/r:System.Messaging.dll
/r:System.Runtime.Remoting.dll
/r:System.Runtime.Serialization.dll
/r:System.Runtime.Serialization.Formatters.Soap.dll
/r:System.Security.dll
/r:System.ServiceModel.dll
/r:System.ServiceModel.Web.dll
/r:System.ServiceProcess.dll
/r:System.Transactions.dll
/r:System.Web.dll
/r:System.Web.Extensions.Design.dll
/r:System.Web.Extensions.dll
/r:System.Web.Mobile.dll
/r:System.Web.RegularExpressions.dll
/r:System.Web.Services.dll
/r:System.Windows.Forms.Dll
/r:System.Workflow.Activities.dll
/r:System.Workflow.ComponentModel.dll
/r:System.Workflow.Runtime.dll
/r:System.Xml.dll
/r:System.Xml.Linq.dll

 

新しいVersion 1.5はオプションダイアログのエディットコントロールをリストボックスにし、最大16のDLLを参照できます。(16というのは、...単なる手抜きです...まぁ、16もあればよいかと...汗; なおプログラム中ではMSCompAss.hの"DLLMAX"という定数に16と定義しているので再コンパイルすればいくつにでも変えられます。)

DLLへのパスは「参照」で追加、「削除」で削除します。またこれに関連して"*.opt"ファイルは、

(最初の一つ目)"RefFile=<ファイルパス>"

(二つ目以降) "RefFile01=<ファイルパス>"、"RefFile02=<ファイルパス>"~"RefFile15=<ファイルパス>"

という(Version 1.4迄と互換性のある)表記にしております。

 

閑話休題

 

この修正を行う為にWEBを調べている際に次のやり取りを見つけました。(出典

(質問者)C#の学習を始めたばかりの初心者です。
低スペックなPCしか持っておらず動作が重い上CPUの負荷を考えてIDEを使っていません。
現在、開発者用コマンドプロンプト(以下cmdと記載します)上で、csc.exeを用いてコンパイルを行っています。
そこで、複数のソースファイル(以下、ソースと記載します)があるとき、cmdでコンパイルする方法はありますでしょうか。

(ベストアンサー)IDE を使ってください。そこまで重くありません。

たとえ起動に 1 分待ったとしても、VSCode で初心者が四苦八苦するくらいなら、十分元は取れて数時間から数万時間は得をします。
私は五年前のノートパソコンに仮想環境を作ってそこに Visual Studio をインストールして使ったことがありますが、十分実用的でした。

初心者がテキストエディタを使って C# の開発をすることには害しかありません。
IDE を使いこなせなければ、学習に数年から数十年は後れをとります。

 

「質問者」さんのお立場や考え方は、ついこの間の私自信と全く同じだったので興味を惹かれました。(ただし、私の場合は多言語、特にC++でのプログラミング歴やアセンブラー、Intel86系CPUの構造や動作を知っている、という差がありますし、「コマンドプロンプト」(DOS窓)で作業するのが嫌でMSCompAssを作った、というところも違いますが...)

 

「ベストアンサー」さんのおっしゃられることはまさにその通りだと思いますが、赤字部分(「害にしか」)には引っ掛かります。私もIDEで開発した方が早いし、簡単だし、手間が省けるのですが、それでも「初めてC#を使う際に、IDEが裏方で何をやっているのかを含めて、知りたい(C#を学習したい)」という気持ちがあったので、あえてIDEを使いませんでした。

"Why? Because ..." 過去にVisual StudioをDLしてC#に触ってみようと思ったことがあるのですが、スケルトンウィザードが勝手に巨大なコードを排出し、それらが何をやっているのかが当時の「C#初心者の私」にはわからず、「IDEに操られている感」が強くて、正直不快に感じたからです。その結果、MSCompAssを作りましたし、自分なりにC++との類似、相違を分析し、C#が内部で(特にCLRをベースにした中間言語を扱うコンパイラーとして)何をやっているのかを知ることが出来たと思います。

 

IDE を使いこなせなければ、学習に数年から数十年は後れをとります。」もその通りであり、使えるならIDEを使った方がよいと思います。またプロのプログラマーが使う機能の豊富なIDEなので、その使い方自体が結構複雑で覚えきるのも大変ということもあります。しかし、ここでいう「学習」の「何を学ぶか」という点に光を当てれば、「IDEをその用法に従って使うだけでは、IDEが裏方でやってくれていることが理解されず(即ちIDEのやっていることは畢竟理解できず)、C#プログラミングの全体像という点での学習」はできないことになると考えます。「IDEを使うな」と言っているわけではありません。「IDEが何をやってくれているのかを理解してIDEを使いなさい」ということを言っているのだとご理解願います。

 

webを見ていると、マネージドリソース vs. アンマネージドリソースやDispose vs. Closeの論議を目にすることがあり、CからC++へハードウェアに近いところでプログラミングをしてきて、Win32 APIの関数を呼ぶ時代から、DLL等を使ったオブジェクトを扱う時代になってもまだ「理解するベース」を学習しておくことは無駄ではない、と考える爺ぃでした。

 

(追記)そうそう、「質問者さん」の私なりの回答はVersion 1.5のヘルプファイルに書いてあります。

 MSCompAssは、基本的に「1つのソースファイルをコンパイルしてビルドする」簡易コンパイルアシスタントツールです。大きなプログラムをいくつかのパーツに分割してそれをDLL化し、メインのプログラムで参照して使うことも出来ます。しかし、多数のソースファイルやmoduleファイルをコンパイル、リンクするにはMicrosoftのVisual Studioをお使いください。(注)

注:DLLは16まで参照出来ます。MSCompAssでDLL化せずに複数アセンブリービルドと行う場合には、サブのソースファイルを予め"/target:module"でコンパイルしておき、メインのソースファイルに"/addmodule:"オプションを使って統合してコンパイルできます。その際は、オプションダイアログの「その他オプション」を使ってください。 

(このmoduleファイルは拡張子が"*.netmodule"と長いです。でも、「その実体は」単なるobjファイルです。(ご参考1ご参考2

 

ps. 本日Version 1.5をアップしたので、1WくらいしたらVectorに上(挙?)がっていると思います。