Albumは単なるSDIウィンドウで余り手の込んだリソースは使っていません。
まず、アイコンはフリーの「本」みたいなやつを使い、ツールバービットマップもWindowsのデフォルトのものと、過去作成したものでほぼ間に合い、唯一サムネイル表示だけTBEditorで作成しました。(下手ですが...汗々;)

ダイアログは3つ。いつもコピペして使うバージョンダイアログは別とし、写真の裏書コメントを書くダイアログは(サイズ可変で最小サイズの制限を設けましたが)エディットボックス一つだけの簡素なものです。一番ごちゃごちゃしているのは「サイズ変更画像の保存」ダイアログ(注)でしょうか?
注:IDD_RESIZEですが、DLLで使うResizerとは別物で、「単一写真の回転状態を変更できるサイズ変更可能な写真画像コピーツール」です。


メニューも表示が「サムネイル表示」と「標準表示」の2モードあり、それぞれに独自のメニューアイテムがあるので結構数が増えてしまいました。また、プログラムの表示モードや、データ読み込み状態と併せてチェックが付いたり、グレイ状態だったりするのがやや目立つところかもしれません。

【Album.rc】
//-----------------------------------------
//             BCCForm Ver 2.41
//    An Easy Resource Editor for BCC
//  Copyright (c) February 2002 by ysama
//-----------------------------------------
#include    "ResAlbum.h"    //(解説:ResAlbum.hは番号を振っているだけなので割愛します。)

//----------------------------------
// ダイアログ (IDD_RESIZE)
//----------------------------------
IDD_RESIZE DIALOG DISCARDABLE 0, 0, 270, 180
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_THICKFRAME | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | DS_SETFONT | DS_CENTER
CAPTION "サイズ変更画像の保存"
FONT 9, "MS 明朝"
{
 CONTROL "完了", IDOK, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 198, 160, 60, 16
 CONTROL "キャンセル", IDCANCEL, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON, 11, 160, 60, 16
 CONTROL "", IDC_TRACKBAR, "MSCTLS_TRACKBAR32", WS_CHILD | WS_VISIBLE | WS_TABSTOP | TBS_AUTOTICKS, 6, 102, 255, 24
 CONTROL "現在の選択ファイル", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 3, 3, 96, 12
 CONTROL "", IDC_SOURCE, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | ES_AUTOHSCROLL | ES_READONLY | ES_LEFT, 3, 17, 262, 15, WS_EX_CLIENTEDGE
 CONTROL "原寸幅", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 15, 36, 45, 12
 CONTROL "", IDC_WIDTH, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | ES_AUTOHSCROLL | ES_READONLY | ES_LEFT, 64, 34, 60, 15, WS_EX_CLIENTEDGE
 CONTROL "原寸高さ", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 135, 36, 45, 12
 CONTROL "", IDC_HEIGHT, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | ES_AUTOHSCROLL | ES_READONLY | ES_LEFT, 190, 34, 60, 15, WS_EX_CLIENTEDGE
 CONTROL "回転状況", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 15, 58, 60, 12
 CONTROL "", IDC_ROTATE, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | ES_AUTOHSCROLL | ES_READONLY | ES_LEFT, 64, 57, 60, 15, WS_EX_CLIENTEDGE
 CONTROL "画像形式", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 135, 58, 60, 12
 CONTROL "", IDC_IMGTYPE, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | ES_AUTOHSCROLL | ES_READONLY | ES_LEFT, 190, 57, 60, 15, WS_EX_CLIENTEDGE
 CONTROL "画像縮小調整割合", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 3, 82, 96, 12
 CONTROL "調整幅", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 15, 135, 45, 12
 CONTROL "", IDC_ADJW, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | ES_AUTOHSCROLL | ES_READONLY | ES_LEFT, 64, 133, 60, 15, WS_EX_CLIENTEDGE
 CONTROL "調整高さ", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 135, 135, 45, 12
 CONTROL "", IDC_ADJH, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | ES_AUTOHSCROLL | ES_READONLY | ES_LEFT, 190, 133, 60, 15, WS_EX_CLIENTEDGE
}
//(解説:最近スライダー(トラックバー)づいており、今回も画像縮小割合はスライダーを使いました。)
 

//----------------------------------
// ダイアログ (IDD_EDIT)
//----------------------------------
IDD_EDIT DIALOG DISCARDABLE 0, 0, 300, 200
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_THICKFRAME | WS_CAPTION | DS_CENTER | DS_SETFONT
CAPTION "写真の裏書編集"
FONT 10, "MS 明朝"
{
 CONTROL "", IDC_EDIT, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL | ES_MULTILINE | ES_WANTRETURN | ES_AUTOVSCROLL| ES_LEFT, 0, 0, 0, 0, WS_EX_CLIENTEDGE
 CONTROL "終了", IDOK, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 256, 184, 40, 14
}

//----------------------------------
// ダイアログ (IDD_VERSION)
//----------------------------------
IDD_VERSION DIALOG DISCARDABLE 0, 0, 160, 40
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_VISIBLE | WS_CAPTION | DS_MODALFRAME | DS_3DLOOK | DS_CENTER
CAPTION "バージョン情報"
FONT 9, "Times New Roman"
{
 CONTROL IDI_ICON, 0, "STATIC", SS_SUNKEN | SS_ICON | WS_CHILD | WS_VISIBLE, 12, 10, 32, 32
 CONTROL "Album Version 1.0\nCopyright 2022 by Ysama", 0, "STATIC", SS_CENTER | SS_SUNKEN | WS_CHILD | WS_VISIBLE, 42, 8, 80, 24
 CONTROL "OK", IDOK, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 130, 14, 20, 12
}

//-------------------------
// メニュー(IDM_MAIN)
//-------------------------
IDM_MAIN MENU DISCARDABLE
{
    POPUP "ファイル(&F)"
    {
        MENUITEM "アルバムの作成、編集(&N)", IDM_NEW
        MENUITEM "アルバムを開く(&O)", IDM_OPEN
        MENUITEM "アルバムの保存(&S)", IDM_SAVE, GRAYED
        MENUITEM "名前をつけて保存(&A)", IDM_SAVEAS, GRAYED
        MENUITEM SEPARATOR
        MENUITEM "終了(&X)", IDM_EXIT
    }
    POPUP "編集(&E)"
    {
        MENUITEM "写真の追加(&A)", IDM_ADD
        MENUITEM "写真の削除(&D)", IDM_DEL, GRAYED
    }
    POPUP "表示(&V)"
    {
        POPUP "サムネイル表示", GRAYED
        {
            MENUITEM "サムネイル大(&L)", IDM_LARGE
            MENUITEM "サムネイル小(&S)", IDM_SMALL
            MENUITEM SEPARATOR
            MENUITEM "標準表示(&N)", IDM_NORM
        }
        POPUP "標準表示", GRAYED
        {
            MENUITEM "原寸表示(&O)", IDM_ORIG
            MENUITEM "サイズ調整表示(&A)", IDM_ADJT
            POPUP "回転表示"
            {
                MENUITEM "0度回転表示", IDM_0, CHECKED
                MENUITEM "90度回転表示", IDM_90
                MENUITEM "180度回転表示", IDM_180
                MENUITEM "270度回転表示", IDM_270
            }
            MENUITEM SEPARATOR
            MENUITEM "サイズ変更画像の保存(&R)", IDM_RESIZE
            MENUITEM SEPARATOR
            MENUITEM "写真の裏書編集(&E)", IDM_EDIT
            MENUITEM "写真の詳細情報(&D)", IDM_DETAIL
            MENUITEM SEPARATOR
            MENUITEM "サムネイル表示(&T)", IDM_THUMNAIL
        }
        MENUITEM "前へ(&P)", IDM_PREV, GRAYED
        MENUITEM "次へ(&N)", IDM_NEXT, GRAYED
    }
    POPUP "ツール(&T)"
    {
        MENUITEM "アルバム写真をフォールダーに纏める(&G)", IDM_GATHER
        MENUITEM SEPARATOR
        MENUITEM "Resizer(&R)", IDM_RESIZER
    }
    POPUP "ヘルプ(&H)"
    {
        MENUITEM "Albumについて(&H)", IDM_HELP
        MENUITEM "バージョン情報(&V)", IDM_VERSION
    }
}

//(解説:CHECKEDとかGRAYEDとか指定されているところに注意です。これが初期化になります。)

//--------------------------
// イメージ(IDI_TOOLBAR)
//--------------------------
IDI_ICON    ICON    DISCARDABLE    "C:\Users\(パス)\Album\Icon.ico"

//--------------------------
// イメージ(IDI_TOOLBAR)
//--------------------------
IDI_TOOLBAR    BITMAP    DISCARDABLE    "C:\Users\(パス)\Album\ToolBar.bmp"

まぁ、リソースエディターが無いのでBCCFormをWin32APIとSDKで力づくで作ったのがWin32 プログラミングの始まりでしたが、慣れてくると過去のリソースのコピペばっかりして、余り独創的なレイアウトを作らなくなりますね。反省です。

 

もうお気づきのことと思いますが、本ブログは、

 

ジャンル「プログラミング」→コードを書いてまじめに解説します。

ジャンル「ブログ」→BCCForm andBCCSkeltonの告知なども行いますが、プログラミングに触れても単なる無駄話や駄弁りの内容です。

 

にし、興味のある人とない人が一目でわかるように「【】書きで大体どんな話か」をタイトルに書こうとしています。

 

このところAlbumの長~いコードを書いているので、(特にスマホで見た時など)ゲロ吐きそうになり、「こりゃ、いかん」ということで「人間の話」を書いてみます。

 

前にも書いたように、私のプログラミング歴はもう40年くらいになりますが、プログラミングスタイルは大体同じで、寝ていても「思いついたら目が覚めて夜中でもキーボードに向かっちゃう方」です。しかし、さすがに歳を重ね、今はベッドで着想がわいた時も、スマホで自分に要点をメールしたりするだけで、若い時のような無茶はできません。しかし、今朝は結構「これだっ!」という着想がわき、朝っぱら(5時前)からチャカチャカやっていたのですが、その後風呂に入って(朝風呂はもう17年以上になります)、コーヒー飲んで食事して、「あれっ?ダメじゃん」ということで、今午後になってから朝書いたコードを基に直しています。

 

これが歳なんでしょうか?

 

若い時は神がかり的着想で、将に「降りてきた」という感じで、後日自分のコードを眺めても「何これ、すごいじゃん、俺って」と感嘆するようなことがありましたが、今は「なにこれ?完全にボケているじゃん!」「なんでこんなコード書いたんだろう?」ということが増えてきました。

 

これが歳なんでしょうか?

 

大体、ミスや失敗なんて肥やし程度にしか考えていなかったのですが、「あ"~、またやり直しかよ」と思うと心身共に心底くたびれます。

 

これが歳なんでしょうか?

 

といって愚痴を垂れます。

 

これが歳なんでしょうか?

 

...なんて凹んでいても仕方がないので、次回はAlbumのリソースをやりましょう。

 

ps. とはいえ、当初やりたかった課題はやりつくしたので一応人様の前に出してもよいアルバムソフトができたと思っています。(今ヘルプを書いています。本ブログがヘルプに載るので、書き終えたらResizerと共に新しいBCCForm and BCCSkeltonのパッケージでアップします。)

 

 

前回に引き続き、CALBUMクラス説明をメンバー関数定義について行います。

【CALBUMのメンバー関数定義部】
//Album(Graphics)の初期化(1DCに1回だけ実施する)
bool CALBUM::Init(HWND hWnd) {
//(解説:この関数は「仮想ウィンドウ」と呼ばれるコンパチDCに読み込ませたフルスクリーンのコンパチビットマップ(m_hCanvas)を作成して、WM_PAINTに集中する描画処理をこのビットマップを使って平準化します。そのプロセスはコメントを追ってご理解ください。)


    //ウィンドウ関係
    m_hWnd = hWnd;                                    //ウィンドウハンドルを記録
    HDC hDC = GetDC(hWnd);                            //対象ウィンドウのDCを取得
    m_hDC = CreateCompatibleDC(hDC);                //コンパチDCを作成
    m_Left = m_Top = 0;                                //フルスクリーンに拡大した
    m_Right = GetSystemMetrics(SM_CXFULLSCREEN);    //時のキャンヴァスサイズを
    m_Bottom = GetSystemMetrics(SM_CYFULLSCREEN);    //記録しておく
    m_hCanvas = CreateCompatibleBitmap(hDC,            //m_hDCではないので注意
                m_Right, m_Bottom);                    //m_hDCにするとモノクロとなる
    SelectObject(m_hDC, m_hCanvas);                    //キャンバスの実体
    PatBlt(m_hDC, m_Left, m_Top, m_Right, m_Bottom, PATCOPY);    //初期描画
    ReleaseDC(hWnd, hDC);                            //hDCを開放
//(解説:仮想ウィンドウが出来たら、そのデバイスコンテキストでGraphicsクラスオブジェクトを作ります。即ち、今後のGDI+処理は仮想ウィンドウに対して行うことになります。)

    if(m_pGraphics = new Graphics(m_hDC))
        return TRUE;
    else
        return FALSE;
}

//出力先ウィンドウのWM_SIZEで実行
void CALBUM::GetSize(LPARAM lParam, HWND TBar, HWND SBar) {
//(解説:この関数はWM_SIZEの処理(BCCSkeltonの"OnSize"関数)において、メンバー変数のm_Y、m_Widthおよびm_Height(ツールバー、ステータスバーがあればその高さを反映させて)を求める処理を行います。m_Xは既に0で初期化されているので登場しません。)
    m_Width = LOWORD(lParam);
    m_Height = HIWORD(lParam);
    RECT rec;
    if(TBar) {
        GetWindowRect(TBar, &rec);
        m_Y = rec.bottom - rec.top;
        m_Height -= m_Y;
    }
    if(SBar) {
        GetWindowRect(SBar, &rec);
        m_Height -= rec.bottom - rec.top;
    }
}

//m_hWndウィンドウのWM_PAINTメッセージで実行
void CALBUM::OnPaint(HDC hDC, int x = 0, int y = 0) {
    //ウィンドウを仮想ウィンドウ(Bitmap)で描画
    BitBlt(hDC, m_Left, m_Top, m_Right, m_Bottom, m_hDC, x, y, SRCCOPY);
}
//(解説:この関数もWM_PAINTの処理(BCCSkeltonの"OnPaint"関数)において、描画された「仮想ウィンドウ」を実際のウィンドウに張り付けます。この為に描画プロセスが見えたり、ちらつきが出たりしないで画面が変わるように見えます。)


//描画色設定(Alpha, R, G, B)
void CALBUM::SetCol(int Alpha, int R, int G, int B) {

    m_Col = Color(Alpha, R, G, B);        //Alpha, R, G, B
}

//(解説:単にRGBの値で色を選択するだけですが、GDI+では更に透明感を出すαブレンドが加わります。)


//画面消去(直前のSetCol関数で消去色を設定する)
bool CALBUM::Cls() {

    if(!m_pGraphics)
        return FALSE;
    m_pGraphics->Clear(m_Col);
    //再描画を要求
    InvalidateRect(m_hWnd, NULL, TRUE);
    return TRUE;
}

//(解説:GDI+のClearメソッドを使っています。ただそれだけでは描画されないのでInvalidateRectを使って画面を書き換えます。主としてサムネイル表示と標準表示の切り替えに使います。)


//ペン設定(直前のSetCol関数で消去色を設定する)
void CALBUM::SetPen(int width) {

    if(m_pPen)
        delete m_pPen;
    m_pPen = new Pen(m_Col, width);
}

//(解説:ペンオブジェクトの選択です。色と太さを指定します。)


//指定(x, y, w, h)の矩形を描画(要色、ペン設定)
bool CALBUM::Rect(int x, int y, int w, int h) {

    if(!m_pGraphics)
        return FALSE;
    m_pGraphics->DrawRectangle(m_pPen, x, y, w, h);
    //再描画を要求
    InvalidateRect(m_hWnd, NULL, TRUE);
    return TRUE;
}

//(解説:GDI+のDrawRectangleメソッドです。ペンオブジェクトでx、y、w、hで示される矩形を、同じくInvalidateRextを使い描画します。具体的にはサムネイル表示の際の選択写真を囲む処理で使います。)


//フォトを追加(ファイル名がNULLだと「ファイルを開く」ダイアログが開く)
bool CALBUM::Add(char* fn, char* ftr = IMGFILTER, char* cmt = "") {
//(解説:コメントの通りですが、この処理はデフォルトで定義炭のGDI+のフィルター(IMGFLT)を使い、コメントはNULLになっています。)
 

    if(*fn)    //ファイル名が与えられた場合
        lstrcpy(m_FileName, fn);
    else {
        //イメージファイルを選択
        OPENFILENAME ofn;                //オープンファイルダイアログ構造体
        ZeroMemory(&ofn, sizeof(ofn));    //ゼロ初期化
        ofn.lStructSize        = sizeof(ofn);
        ofn.hwndOwner        = m_hWnd;
        ofn.lpstrFilter        = ftr;        //引数でフィルタリングする
        ofn.nFilterIndex    = 1;
        ofn.lpstrFile        = m_FileName;
        ofn.nMaxFile        = MAX_PATH;
        ofn.lpstrTitle        = "イメージファイルの選択";
        ofn.lpstrInitialDir = ".";
        ofn.Flags            = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT;
        ofn.lpstrDefExt        = NULL;
        if(!GetOpenFileName(&ofn)) {
            return FALSE;
        }
    }

//(解説:ここまででマルチバイト文字のm_FileNameが確定します。)
    //写真を追加
    m_Photo[m_NoI] = new (CPHOTO);    //写真(CPH0TOインスタンス)の作成
    m_Photo[m_NoI]->SetPhoto(m_FileName, cmt);
//(解説:CPHOTOオブジェクトをnewで一つ増やし、それを配列の次<新規>を指しているm_NoIにSetPhoto関数でデータを与えます。)
    //写真数を増やす
    m_NoI++;
    if(m_NoI > MAXIMG) {
        MessageBox(m_hWnd, "これ以上のイメージ読み込みはできません", "警告", MB_OK | MB_ICONERROR);
        m_NoI--;
        return FALSE;
    }

//(解説:写真最大数迄このように増やしてゆきます。)
    return TRUE;
}

//フォトの削除
bool CALBUM::Delete(int n) {

    if(n >= m_NoI || n < 0)    //違法な引数
        return FALSE;
    m_Photo[n]->Delete();
//誤    for(int i = n; n < m_NoI - 1; i++) {

    for(int i = n; i < m_NoI - 1; i++) {
        m_Photo[i] = m_Photo[i + 1];
    }
    if(m_NoI > 0) {
        m_NoI--;
        m_Photo[m_NoI] = 0;

    }
    if(m_Selected && m_Selected >= n)
        --m_Selected;

    return TRUE;
}
//(解説:サムネイルを使っているとおかしな引数が与えられる可能性があるので最初にエラーチェックを入れています。指定された番号の写真オブジェクトを削除し、その一つ上から順繰りに後釜に入れてゆき、最後のポインターをゼロにして、配列数(m_NoI)を一つ減らします。)

//全フォトの削除
void CALBUM::DeleteAll() {

    for(int i = m_NoI - 1; i >= 0; i--)
        delete m_Photo[i];
    m_NoI = 0;                    //読み込みImage数
    m_Selected = 0;                //選択されているイメージの配列番号
    m_CurPage = 0;                //サムネイル表示で現在選択されているページ
    m_RFType = RotateNoneFlipNone;    //回転、反転なし
}
//(解説:単純に最新の写真から最初の写真迄削除し、変数を初期化します。)

//選択イメージの設定(0~ベース)
void CALBUM::Select(int n) {

    if(n < 0)
        m_Selected = 0;
    else {
        if(n < m_NoI)
            m_Selected = n;
        else
            m_Selected = m_NoI - 1;
    }
}
//(解説:何番の写真が選択されているかを示す変数m_Selectedに値を入れます。0未満が指定されれば先頭の写真、写真数を超えれば最後の写真を選択します。)

//選択イメージ幅(LOWORD)、高さ(HIWORD)の取得
DWORD CALBUM::GetImgSize() {

    //エラー対応
    if(!m_Photo[m_Selected])
        return 0;
    //現在選択されている写真のファイル名をワイド文字にする
    mbstowcs(m_WFName, m_Photo[m_Selected]->m_FName, MAX_PATH);
    //現在選択されている写真のファイル名でイメージクラスインスタンスを生成
    m_Image = new (Image)(m_WFName);
    //m_Imageインスタンスの生成に失敗した場合
    if(!m_Image)
        return 0;
    //イメージ幅(LOWORD)、高さ(HIWORD)を取得
    DWORD size = MAKELONG((WORD)m_Image->GetWidth(), (WORD)m_Image->GetHeight());
    //m_Imageインスタンスを解放し、ポインターをゼロ初期化
    delete m_Image;
    m_Image = 0;
    return size;
}
//(解説:写真画像の描画関係処理では、未生成の写真が選ばれるとすぐにプログラムが落ちるので最初に「エラー対応」をします。また、画像の幅、高さを取得する為に、わざわざGDI+のImageクラスインスタンスを生成してGetWidth/Heightメソッドで取得するので、いちいち一方だけ処理するのは不経済なのでDWORDで一緒に渡すようにしました。)

//m_Selectedのファイルのフルパスファイル名を返す
char* CALBUM::GetFileName() {

    return m_Photo[m_Selected]->m_FName;
}

//選択イメージのコメントの取得
char* CALBUM::GetComment() {

    return m_Photo[m_Selected]->m_Comment;
}

//選択イメージのコメントの設定
void CALBUM::SetComment(char* cmt) {

    m_Photo[m_Selected]->SetComment(cmt);
}
//(解説:元々はCPHOTOクラスに設定したメンバー関数ですが、画像のメンバー変数を外す際にCALBUMクラスへ引っ越しました。)

//イメージの表示(x, yからw幅、h高さの矩形に原寸表示比率で表示)
bool CALBUM::ShowImg(int x, int y, int w, int h) {
//(解説:ImageクラスのDrawImageメソッドは30もオーバーロード関数があり、サムネイルも別途関数があるのですが、オリジナルの画像の現寸縦横比のまま指定矩形の中央に最大サイズで描画する機能が見当たらなかったので自分で書きました。)

    //エラー対応
    if(!m_Photo[m_Selected])
        return FALSE;
    //現在選択されている写真のファイル名をワイド文字にする
    mbstowcs(m_WFName, m_Photo[m_Selected]->m_FName, MAX_PATH);
    //現在選択されている写真のファイル名でイメージクラスインスタンスを生成
    m_Image = new (Image)(m_WFName);
//(解説:描画の際にのみCPHOTOのm_FNmaeファイルパスで指定される画像ファイルでImageクラスオブジェクトを生成します。また、他のCOMと同様、ファイル名はワイド文字(wcs)で与えなければなりません。)
    //m_Imageインスタンスの生成に失敗した場合
    if(!m_Image)
        return FALSE;
    //画像を回転させる-RotateFlipType:Rotate<A>Flip<B> <A:None, 90, 180, 270>, <B:X, Y, XY>
    m_Image->RotateFlip(m_RFType);
//(解説:最初はアルバムの表示はオリジナルの回転なしでしたが、意外に回転しないと縦にならない写真が多いので、後から加えた機能です。回転はRotaryFlipTypeと呼ばれるenum変数で表され、"Rotary180FlipX"のように表示されます。これをアルバムの表示用変数にしたのがm_RFTypeです。)
    //イメージの原寸を取得
    int imgw = m_Image->GetWidth();
    int imgh = m_Image->GetHeight();
//(解説:まず原寸の幅と高さを取ります。)

    //w、h内で原寸の縦横対比のイメージを中央に配置
    double asp1, asp2;
    asp1 = (double)imgh / (double)imgw;
    asp2 = (double)h / (double)w;

//(解説:整数をdoubleにキャストして原寸の縦横比と表示矩形の縦横比を求めます。)

    if(asp1 > asp2) {
        imgh = h;
        imgw = h / asp1;
    }
    else {
        imgw = w;
        imgh = w * asp1;
    }

//(解説:それ等を比較して長い方が表示矩形の一辺とし、原寸の比率で他の一辺の長さを決めます。)

    x += (w - imgw) / 2;
    y += (h - imgh) / 2;

//(解説:表示は中央表示にします。)

    //m_Imageインスタンスを表示
    m_pGraphics->DrawImage(m_Image, x, y, imgw, imgh);

//(解説:DrawImageは最もオーソドックスなx、y、w、hでの指定にしました。)

    //m_Imageインスタンスを解放
    delete m_Image;
    m_Image = 0;

//(解説:以前はCPHOTOオブジェクトで保持していましたが、メモリーリソースの節約のために、Imageインスタンスは使ったら使い捨てて、表示したら毎回廃棄します。)

    //再描画を要求
    InvalidateRect(m_hWnd, NULL, TRUE);

//(解説:お約束の描画要求です。)

    return TRUE;
}

//イメージの表示(第1引数TRUE左上、FALSE中央、第2引数TRUE原寸、FALSEサイズ調整)
bool CALBUM::ShowImg(bool lefttop, bool asis) {

//(解説:以下は↑の表示の相方にあたるもので、表示位置を左上、サイズを原寸のまま表示できるようにしたものです。)

    //エラー対応
    if(!m_Photo[m_Selected])
        return FALSE;
    //現在選択されている写真のファイル名をワイド文字にする
    mbstowcs(m_WFName, m_Photo[m_Selected]->m_FName, MAX_PATH);
    //現在選択されている写真のファイル名でイメージクラスインスタンスを生成
    m_Image = new (Image)(m_WFName);
    //m_Imageインスタンスの生成に失敗した場合
    if(!m_Image)
        return FALSE;
    //画像を回転させる-RotateFlipType:Rotate<A>Flip<B> <A:None, 90, 180, 270>, <B:X, Y, XY>
    m_Image->RotateFlip(m_RFType);
    //イメージの原寸を取得
    int imgw = m_Image->GetWidth();
    int imgh = m_Image->GetHeight();
    //サイズ調整の場合は比率に応じて調整
    if(!asis) {
        double asp1, asp2;
        asp1 = (double)imgh / (double)imgw;
        asp2 = (double)m_Height / (double)m_Width;
        if(asp1 > asp2) {
            imgh = m_Height;
            imgw = m_Height / asp1;
        }
        else {
            imgw = m_Width;
            imgh = m_Width * asp1;
        }
    }
    //左上に表示
    int x = m_X;
    int y = m_Y;
    //中央に表示
    if(!lefttop) {
        x += (m_Width - imgw) / 2;
        y += (m_Height - imgh) / 2;
    }
    //m_Imageインスタンスを表示
    m_pGraphics->DrawImage(m_Image, x, y, imgw, imgh);
    //m_Imageインスタンスを解放
    delete m_Image;
    m_Image = 0;
    //再描画を要求
    InvalidateRect(m_hWnd, NULL, TRUE);
    return TRUE;
}

//サムネイル表示(引数はサムネイルの幅・高さ、ページ)
bool CALBUM::ShowThumbnail(int size, int page = -1) {

//(解説:ImageクラスにGetThumbnailImageメソッドがあるのですが、これも原寸の縦横比を変えられるのでDrawImageで縮小表示するのと何ら変わりなく、最初の自作ShowImage関数を使っています。この変数ではサムネイルの描画フレーム矩形サイズを指定し、ページ番号を指定することができます。ページ番号はデフォルト値が決められており、指定されないと「選択された写真が表示されるページ」が表示されます。)

    //エラー対応
    if(!m_NoI)
        return FALSE;
    //イメージ表示の為の画面数、横表示数、縦表示数設定
    int sel = m_Selected;                        //m_Selectedを退避保存
    m_Column = m_Width / size;                    //列数(1~ベース)
    m_Row = m_Height / size;                    //行数(1~ベース)
    if(!(m_Column * m_Row)) {
        MessageBox(m_hWnd, "ウィンドウサイズが小さすぎます",
                    "警告", MB_OK | MB_ICONERROR);
        return FALSE;                            //"Div. by zero" error
    }

    m_Page = m_NoI / (m_Column * m_Row);        //ページ数(1~ベース)
    if(page == -1)                                //現在選択されている写真のページ
        m_CurPage = page = m_Selected / (m_Column * m_Row);
    else
        m_CurPage = page;
//(解説:ここまでがパラメーター確定の処理です。)

    if(page > m_Page)                            //page引数が大きすぎて表示できない
        return FALSE;

//(解説:ここからサムネイル表示と選択画像を囲む矩形を表示する処理です。Select関数でm_Selectedが変わるのでその値をsel変数に一旦退避し、処理が終了する際に戻しています。)

    for(int i = 0; i < m_Row; i++) {
        for(int j = 0; j < m_Column; j++) {
            int n = page * (m_Column * m_Row) + i * m_Column + j;
            if(n < m_NoI) {
                Select(n);
                ShowImg(size * j, size * i + m_Y, size, size);
                if(n == sel) {
                    Rect(size * j, size * i + m_Y, size, size);
                }
            }
            else {
                m_Selected = sel;
                return TRUE;
            }
        }
    }
    m_Selected = sel;
    return TRUE;
}

//サムネイル表示のページ当り表示写真数
int CALBUM::HowManyThumb(int size) {

    m_Column = m_Width / size;    //列数(1~ベース)
    m_Row = m_Height / size;    //行数(1~ベース)
    return (m_Column * m_Row);
}

//(解説:この関数は「前へ」「次へ」で選択写真を1ページ分移動する為に使います。この処理のために、m_Selectedがm_NoIを超えたり、0未満になったりすることがあり得るのです。)


//出力先ウィンドウのx, y座標からサムネイルの番号を得る
int CALBUM::GetWhichThumb(int size, int x, int y) {

    int col = x / size;
    int row = y / size;
    return (m_CurPage * (m_Column * m_Row) + row * m_Column + col);
}

//(解説:これはマウス左クリックで写真を選択する際に画面の位置から選択写真の配列引数を求める為に使います。)

 

如何でしたでしょうか?CPHOTOクラス、CALBUMクラスで基本的な「写真の選択、読み込み、表示」と「複数写真の管理」ができたので、これをスケルトンに組み込んでゆきます。(注)

注:実は単なるアルバム写真の表示では飽き足らなくなり、画像ファイルの加工やその詳細データの表示機能もアルバムに付け加えてゆくことになりますが、それはスケルトンの解説で紹介します。

 

今朝、フォールダーに入っているGDI+が書き込み処理可能な画像ファイル(注)のサイズをi一定の条件に沿って一括変換するResizerというソフトを書きあげました。

注:"image/bmp", "image/jpeg", "image/gif", "image/tiff", "image/png"の5つですね。

 

が、

 

昨日以来今朝に至るまで「Imageクラスインスタンスを作った段階でプログラムが落ちる」トラブルで頭を悩ませていました。そしてその原因が(頭の悪い私の)おかしな思い込みによるものであることが分かりました。

トラブルというのは、SkeltonWizardでつくったダイアログベースのスケルトンで、GDI+を使おうとして何の迷いもなく、

 

CMyWnd::CMyWnd() {         //コンストラクター

    //ワイド文字使用のためロケールの初期化(日本語)
    setlocale(LC_CTYPE, "JPN");
    //GDI+の開始
    GdiplusStartup(&m_gdiToken, &m_gdiSI, NULL);
}

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

    //GDI+の終了
    GdiplusShutdown(m_gdiToken);
}

 

とやってテストをすると落ちる、落ちる、落ちまくります。この為、色々なところに私特有のMessageBoxトラップをかけて全てGDI+関連のところで落ちていることを確認しますが、(自分ではちゃんと初期化を行っているつもりなので)「何故!?!?!?!」と考えてしまいます。最後にImageクラスポインターにnewでImageオブジェクトを作るところのファイル名(ワイド文字)を標示したら「setscaleを実施しているにもかかわらず」ファイル名がおかしい!ということで↑のコンストラクターとデストラクターにトラップを仕掛けると、デストラクターは働いていますが、コンストラクターはスルーされています。

 

???............!!!

 

BCCSkeltonの、SkeltonWizardで作成されるウィンドウ(ダイアログ)クラスは、宣言部で

class CMyWnd : public CDLG
{
public:    //以下はコールバック関数マクロと関連している
    //////////////
    //メンバー変数
    //////////////
    //2重起動防止用のMutex用ID名称
    CMyWnd(char* UName) : CDLG(UName) {}

と「文字列を渡すコンストラクター(注)」を宣言しています。

注:このコンストラクターは、派生元のCDLGクラスやCSDIクラス等のミューテックスを利用した排他処理の可否を問うIsOnlyOne関数の為にユニークな名称を文字列として与えるコンストラクターになっています。

従って、↑のような引数を与えないデフォルトコンストラクターを作っても、インスタンスを作るSkeltonWizardのコードは、

 

CMyWnd Resizer("Resizer");    //ウィンドウクラスインスタンスの生成
 

「文字列を引数に渡すコンストラクター」を使っており、実は全く呼ばれないのです。(20年も経ったので、自分で作ったものの、もはや忘れかけており、恥ずかしい限りです。)

 

ということで、教訓。

SkeltonWizardでつくったBCCSkeltonのコードを使う場合、(GDI+の「GdiplusStartup」や「GdiplusShutdown」のような)始めとお終いに必ず行う処理はOnCreateやOnInit、OnClose等のウィンドウ処理の「必ず通る関数」(注)に仕掛けた方がよいでしょう。

注:なおダイアログの場合、OnIdokやEscキーを押したときも想定してOnIdcancelに仕掛けても、ダイアログの「X]ボタンを押した際にはスルーされるので、必ずOnCloseに仕掛けるよう気を付けましょう。

 

追補:【ちょっといい話】

この話を書く際にウェブをチェックしていて、

デフォルトコンストラクタが呼ばれない

という記事を見つけました。ありがちな話ですよね?(忘れずに一番下のAkira Takahashiさんのアドバイスをお読みください。)

BCC55はC++11以前、BCC102はC++11適合なので実験してみても面白いと思います。

 

追々補:【ちょっといい話】

Resizerで、料理の写真(一辺が2~4000ピクセルで1枚5MB、全部で260MB程度)を纏めて「長い方の1辺を960ピクセルにしてオリジナルの縦横対比を変えない」ように縮小したら、解像度は十分ありながら1枚650KB、全部で24MB程度にダウンサイジングができました。おかげさまでAlbumもサクサク動いています。

CALBUMクラスは、Albumというアプリケーションの為のクラスであり、汎用性は乏しいと思います。一方その量は結構あるので説明の仕方が難しいです。
そんなことで、CALBUMクラスの説明は宣言部を総論で「大体どんな風になっているか?」を説明し、次回に各論としてメンバー関数の定義部を述べてみようかと思います。なお、コンストラクターとデストラクターはメンバー関数ですが概要説明のために宣言部で説明します。

先ずCALBUMクラスは基本設計、仕様に基づく複数の写真(CPHOTOクラスオブジェクト)を扱い(データ管理機能)、写真を一覧表示や単一拡大表示する(GDI+を利用した表示機能)アプリケーションですので、この機能をCALBUMで実装します。
 

「データ管理機能」といっても、普通アルバムの写真数は多くなく(多すぎると逆にユーザーの負担になる)、CALBUMでも1アルバムの最大写真数を(とりあえず)512枚にして、写真の追加、削除だけでよいと考えました。なお、データファイルはFileList(DLL版)でも扱うため、FileListで検索、並び替え(といってもデータ要素が「ファイル名」と「コメント」ではキーになり得ませんが...)ができるようにします。
「表示機能」についてはGDI+を使った一覧表示(以下「サムネイル表示」)や単一拡大表示(以下「標準表示」)を行い、その為の下働きはCALBUMクラスで行うことと、サムネイル表示等写真のサイズが大きいと処理に時間がかかり、描画しているところを見せると無様なので(CANVASクラスと同じく)コンパチビットマップによる「仮想ウィンドウ」を利用しています。

【CALBUMの宣言部】
///////////////////////////////
//CALBUMクラス(based on GDI+)
//Copyright (c) 2022 by Ysama
//GDI+を使ったイメージファイル
//の管理ユーティリティ
//注意:アルバムの使用にはGDI+、
//フォトクラスのヘッダー取込み
//が必要である。
///////////////////////////////
//イメージファイルのフィルター
#define        IMGFILTER    "イメージファイル\0*.bmp;*.jpeg;*.jpg;*.gif;*.tiff;*.png;*.wmf;*.emf;*.ico\0\0"

//(解説:GDI+の対象となる「ファイルを開く」ダイアログ用ですが、CEXTCHKクラスでも使います。)

//エンコーダーリスト
const WCHAR* g_EncoderList[5] = {L"image/bmp", L"image/jpeg", L"image/gif", L"image/tiff", L"image/png"};
//(解説:GDI+のエンコーダーリストを配列で用意しておきます。)

//GDI+のインクルード、ネームスペースの使用
#include    <gdiplus.h>
using namespace Gdiplus;

//(解説:GDI+の利用にあたって、これは不可欠です。)
//ワイド文字使用の為"wchar.h"をインクルード
#include <wchar.h>
//(解説:これもGDI+-というかCOM一般-の利用に不可欠です。)

//CPHOTOクラスヘッダー
#include    "CPHOTO.h"

//(解説:CPOTOクラスオブジェクトが「写真」を表すこと既にやりましたね。)


//イメージファイルの最大数
#define    MAXIMG    0x200                        //512枚の写真-ユーザー変更可

//(解説:512枚でも十分すぎるほどです。)


class CALBUM
{
private:    //(解説:システムに近いところは触らせないようにprivateにしてあります。)
    //GDI+関係メンバー変数
    GdiplusStartupInput m_gdiSI;            //GDI+のスタートアップインプット
    ULONG_PTR m_gdiToken;                    //GDI+のトークン
    Graphics* m_pGraphics = 0;                //Graphicsオブジェクトポインター
    Color m_Col = 0;                        //Colorオブジェクト
    Pen* m_pPen = 0;                        //矩形描画の為のペンオブジェクトポインター
    CPHOTO* m_Photo[MAXIMG] = {0};            //CPHOTOクラスインスタンスポインター配列
    Image* m_Image = 0;                        //Imageクラスオブジェクトポインター
    RotateFlipType m_RFType = RotateNoneFlipNone;    //出力用回転、反転タイプ(回転、反転なし)

//(解説:以上はGDI+に固有の変数やクラスオブジェクトと、CPOTOクラスのポインターです。)

   //出力先ウィンドウ関係メンバー変数
    HWND m_hWnd;                            //出力先ウィンドウのハンドル
    int m_X = 0;                            //出力先ウィンドウ有効クライアントエリアX座標
    int m_Y = 0;                            //出力先ウィンドウ有効クライアントエリアY座標
    int m_Width;                            //出力先ウィンドウ有効クライアントエリア幅
    int m_Height;                            //出力先ウィンドウ有効クライアントエリア高さ
//(解説:以上は出力先ウィンドウの出力に必要なデータの変数です。)

   //出力先仮想ウィンドウ関係メンバー変数
    HDC m_hDC;                                //出力先ウィンドウのコンパチブルDC
    HBITMAP m_hCanvas;                        //仮想ウィンドウであるビットマップ
    int m_Left;                                //仮想ウィンドウ(フルスクリーン)左座標
    int m_Top;                                //仮想ウィンドウ上座標
    int m_Right;                            //仮想ウィンドウ右座標
    int m_Bottom;                            //仮想ウィンドウ下座標

//(解説:以上は仮想ウィンドウ関連の変数です。)

    //サムネイル用変数
    int m_Page = 0;                            //写真表示ページ数(1~ベース)
    int m_Column = 0;                        //写真の列数(1~ベース)
    int m_Row = 0;                            //写真の列数(1~ベース)

//(解説:最後がサムネイル表示に必要な変数です。)

public:
    //表示写真関係メンバー変数
    char m_FileName[MAX_PATH];                //フルパス写真ファイル名用文字列(MBS)
    WCHAR m_WFName[MAX_PATH];                //フルパス写真ファイル名用文字列(WCS)
    int m_NoI = 0;                            //読み込み写真数
    int m_Selected = 0;                        //選択されている写真の配列番号
    int m_CurPage = 0;                        //サムネイル表示で現在選択されているページ(0~ベース)

//(解説:アクセスできる変数としてはファイル名文字列や読込写真数、現在の選択写真や(サムネイル表示の)ページ等です。)

    //メンバー関数
    CALBUM();                                //コンストラクター
    ~CALBUM();                                //デストラクター

//(解説:コンストラクターとデストラクターは以下で説明します。)

    bool Init(HWND);                        //Graphicsクラスと出力仮想ウィンドウの初期化

//(解説:出力先のウィンドウのWM_CREATE辺りで初期化することを想定しています。)

    void GetSize(LPARAM, HWND, HWND);        //出力先ウィンドウのWM_SIZEメッセージ時に実行
    void OnPaint(HDC, int, int);            //出力先ウィンドウのWM_PAINTメッセージ時に実行

//(解説:出力先ウィンドウのOnSizeに入れて必要なデータを取得、OnPaint関数の中に入れて仮想ウィンドウ(ビットマップ)を描画する為のものです。)

    void SetCol(int, int, int, int);        //描画色設定(Alpha, R, G, B)
    bool Cls();                                //画面消去(直前のSetCol関数で消去色を設定する)
    void SetPen(int);                        //ペン設定(直前のSetCol関数で消去色を設定する)
    bool Rect(int, int, int, int);            //指定(x, y, w, h)の矩形を描画(色、ペン設定後実行)
//(解説:イメージ表示以外のグラフィック表示関連です。)

    bool Add(char*, char*, char*);            //写真を追加(ファイル名がNULLだと「ファイルを開く」ダイアログが開く)
    bool Delete(int);                        //写真の削除
    void DeleteAll();                        //全写真の削除

//(解説:CPHOTOクラス配列の管理関連の関数です。)

    RotateFlipType GetRotateFlip()            //現在の回転、反転状態を返す
        {return m_RFType;}
    void SetRotateFlip(RotateFlipType type)    //写真回転、反転表示指定
        {m_RFType = type;}

//(解説:(ちょっと違和感のある位置ですが)写真の「回転表示」に関する関数で、RotateFlipTypeはenum型変数です。)

    int GetSelected()                        //選択写真の配列番号取得(0~ベース)
        {return m_Selected;}
    void Select(int);                        //写真の選択(0~ベース)

//(解説:再度CPHOTOクラス配列の「選択されている」写真の取得、設定に関わる関数です。)

    DWORD GetImgSize();                        //選択イメージ幅(LOWORD)、高さ(HIWORD)の取得

//(解説:「選択されている」写真のサイズ取得に関わる関数です。データは2つ一遍にDWORDで渡されます。)

    char* GetFileName();                    //m_Selectedのファイルのフルパスファイル名(MBS)を返す
    char* GetComment();                        //選択イメージのコメントの取得
    void SetComment(char*);                    //選択イメージのコメントの設定

//(解説:「選択されている」写真のメンバー変数取得、設定に関わる関数です。)

    bool ShowImg(int, int, int, int);        //写真の表示(x, yから幅w、高さh内に原寸縦横対比で中央に表示)
    bool ShowImg(bool, bool);                //イメージの表示(第1引数TRUE左上、FALSE中央、第2引数TRUE原寸、FALSEサイズ調整)
    bool ShowThumbnail(int, int);            //サムネイル表示(引数はサムネイルの幅・高さ、ページ)

//(解説:画像表示関数です。上から「指定位置、矩形に縦横比を保持して中央に配置」、「左上または中央に、原寸またはサイズ調整して表示」、最後は「サムネイルで指定長の正方形の指定ページを表示」する関数です。)

    int GetPage() {return m_CurPage;}        //現在のページを返す
    int HowManyThumb(int);                    //サムネイル表示のページ当り表示写真数
    int GetWhichThumb(int, int, int);        //出力先ウィンドウのx, y座標からサムネイルの番号を得る

//(解説:最後はサムネイル表示の為の、現在のページ取得、1ページの写真数取得および(マウスクリックされた)写真の配列番号を取得する関数です。)

};

//コンストラクター
CALBUM::CALBUM() {

    //ワイド文字使用のためロケールの初期化(日本語)
    setlocale(LC_CTYPE, "JPN");
    //GDI+の開始
    GdiplusStartup(&m_gdiToken, &m_gdiSI, NULL);

}

//(解説:GDI+を使う場合ワイド文字が必須なので、初期化関数とロケール設定はコンストラクターでやるのが効率的です。)


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

    //スクリーンビットマップの削除
    DeleteObject(m_hCanvas);
    //コンパチDCの削除
    DeleteDC(m_hDC);
    //CPHOTOオブジェクトの削除(CPHOTOのメンバーはそのデストラクターで解放)
    for(int i = m_NoI - 1; i >= 0; i--)
        delete m_Photo[i];
    //Penオブジェクトの終了
    if(m_pPen)
        delete m_pPen;
    //Graphicsオブジェクトの終了
    if(m_pGraphics)
        delete m_pGraphics;
    //GDI+の終了
    GdiplusShutdown(m_gdiToken);

}

//(解説:GDI+を使う場合終了関数と生成した(メモリーを割り当てた)オブジェクトを解放する必要があるので、忘れないようにデストラクターで解放することが必要です。)

 

次回はメンバー関数の定義で特徴的なものを説明します。

 

突然ですが、ブログベースで上記を告知します。

 

現在Album Ver 1.0を完了するも、数メガクラスの静止画を多数扱うと動作が重くなるので、どうでもよい写真はサイズを落としてスピードを上げられるように静止画一括縮小コピーソフト、Resizerを開発していますが、その際「フォールダーを選ぶダイアログ」が今のWin10のようなユーザーインターフェースではないことに気が付き、20年ぶりに今風にアップデートしてみました。

 

【旧BrouwseForFolderダイアログ】

・BROWSEINFO構造体のulFlagsの値がBIF_RETURNONLYFSDIRS(ファイルシステムディレクトリーのみ)

「OK」と「キャンセル」しかボタンが無く、右クリックのポップアップメニューによる名前の変更等ができませんでした。また、「新しいフォールダー」を作ることもできませんでした。

 

【新BrouwseForFolderダイアログ】

・BROWSEINFO構造体のulFlagsの値がBIF_USENEWUI(新しいインターフェースの使用)

「OK」と{キャンセル」に加え、「新しいフォールダー」ボタンが増え、右クリックのポップアップメニューによるフォールダー名の変更等ができるようになりました。なお、このダイアログを使うためにはCoInitialize関数によるCOM初期化が必要となりますので、ソースにコメントを付記しておきました。

 

前に書いた通り、GDI+の世界は広いので高望みはしないで、自分が今したいこと(①イメージファイルの表示、②選択を示す矩形描画、③画面消去)だけ調べてみます。どうも入り口と出口の作法(【GDI+】GDI+の作法)、GraphicsクラスとImageクラス(【GDI+】イメージの描画)を使えばやりたいことはできそうです。そうと分かったら、試行錯誤で実際にテストしてみるのが私のプログラミング作法です。

(1)写真クラスはCPHOTOとして、フルパスファイル名とコメント用の可変長文字列とImageクラスオブジェクト用のポインターを持つクラスとして定義。
(2)表示実験は、実際に最後まで使うSDIの「器(スケルトン)」をBCCFormとSkeltonWizardで作成。(これが一番簡単!)
(3)サンプルのjpeg画像を用意して表示実験
を試行錯誤で繰り返します。

CPHOTOクラスは大体次のような形で収まります。

////////////////////////////////
//CPHOTOクラス(based on GDI+)
//Copyright (c) 2022 by Ysama
//GDI+を使ったイメージファイル
//注意:GDI+のヘッダーの取込み、
//GdiplusStartupによる初期化や、
//GdiplusShutdownの終了処理は、
//上位プログラムで行う。
////////////////////////////////

//ワイド文字使用の為"wchar.h"をインクルード   //CALBUMへ移行→削除へ
#include <wchar.h>


class CPHOTO
{
public:
    //メンバー変数
    char* m_FName = 0;            //フルパスファイル名用ポインター

    WCHAR m_WFName[MAX_PATH];    //フルパスファイル名用ワイド文字列→削除へ
    Image* m_Image = 0;            //Imageオブジェクトポインター→削除へ
    char* m_Comment = 0;        //コメント文字列用ポインター
    //メンバー関数

    CPHOTO();                    //コンストラクター→削除へ
    ~CPHOTO();                    //デストラクター
    bool SetPhoto(char*, char*);//イメージを設定する
    bool SetComment(char*);        //コメントを設定する
    void Delete();                //メンバー変数メモリーを解放し、初期化する

    int GetW();                    //選択イメージ幅の取得→削除へ
    int GetH();                    //選択イメージ高さの取得→削除へ
};

//コンストラクター→削除へ
CPHOTO::CPHOTO() {

    //ワイド文字使用のためロケールの初期化(日本語)
    setlocale(LC_CTYPE, "JPN");
}


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

    Delete();    //メモリーを解放する
}

//イメージを設定する
bool CPHOTO::SetPhoto(char* fn, char* cmt = "") {

    //パラメーター異常対応
    if(lstrlen(fn) > MAX_PATH - 1)
        return FALSE;
    //既に設定されている場合再初期化
    if(m_FName)
        Delete();
    //ファイル名の記録
    m_FName = new char[lstrlen(fn) + 1];
    lstrcpy(m_FName, fn);
    //コメントの記録
    SetComment(cmt);

    //ファイル名のワイド文字化→削除へ
    mbstowcs(m_WFName, m_FName, MAX_PATH);→削除へ
    //イメージファイル(m_WFName)を設定→削除へ
    m_Image = new (Image)(m_WFName);→削除へ
    if(!m_Image)→削除へ
        return FALSE;→削除へ
    else→削除へ
        return TRUE;→削除へ

}

//コメントを設定する
bool CPHOTO::SetComment(char* cmt) {

    //既に設定されている場合
    if(m_Comment)
        delete [] m_Comment;
    m_Comment = new char[lstrlen(cmt) + 1];
    lstrcpy(m_Comment, cmt);
    return TRUE;
}

//メンバー変数メモリーを解放し、初期化する
void CPHOTO::Delete() {

    //メンバー変数のメモリーを解放
    if(m_FName) {
        delete [] m_FName;
        m_FName = 0;
    }

    if(m_Image) {            //→削除へ
        delete m_Image;
        m_Image = 0;
    }

    if(m_Comment) {
        delete [] m_Comment;
        m_Comment = 0;
    }
}


//選択イメージ幅の取得→削除へ
int CPHOTO::GetW() {

    if(!m_Image)
        return 0;
    return m_Image->GetWidth();
}

//選択イメージ高さの取得//→削除へ
int CPHOTO::GetH() {

    if(!m_Image)
        return 0;
    return m_Image->GetHeight();
}


尤も、後の「'?'(0x3F)バグ騒動」のおかげで↑の「文字列データとImageデータが混ざった中途半端なオブジェクト」は整理され、GDI+関連は全てアルバムクラスへ移行し、CPHOTOクラスはテキストデータ(要すれば、FileListのデータそのもの)のオブジェクトに特化します。(↑の赤字取り消し線部分が最終的に削除されます。)結果的には綺麗に整理されてよかったです。

 

私たちの世代は、記念、記録と言ったら写真(子供の頃はモノクローム、ですよ)と紙のアルバムでした。初めてビデオカメラを「担いだ」(注)のは、初めて米国に駐在した1991年のことと記憶しています。
注:当時先端のSONYホームビデオカメラでも文字通り、担ぐしかありませんでした。

それから時は流れ、(私の場合)ガラケーの写メ(640x480)、デジカメ(1,600x1,200)へと発展し、現在のスマホのカメラは画素3,264x2,448(デジカメの6倍以上、なんとガラケーの26倍!)まで発展してきました。昔のようにフィルム代や残フィルム数を気にすることがなくなった反面、バシバシとるのでイメージファイルの管理が大変になってきていることも確かです。

そんなわけで、昭和生まれ、育ちの私として、PC内に散らばった画像を「昔のアルバムのように纏めて管理できるようなソフトがあったらいいな」と考えたのは自然な流れでした。(詳細は「【GDI+】うへぇ、広いなぁ」参照)

それではいよいよプログラミングに入ります。
↑の「したいこと」(要求仕様)をブレークダウンして、具体化してゆきます。
(1)オブジェクトプロパティは、画像データを持つ「写真」と、それを複数保持する「アルバム」に分かれます。
(2)動作(メソッド)は、「写真を探す」(画像ファイルの追加、削除)「アルバムに差す」(ファイルの読み込み)「一覧で眺める」(サムネイル表示)「手に取ってよく見る」(単一画像表示-以下「標準表示」といいます)「思い出等忘れないよう備忘を裏に書く」(画像データに関連付けられたテキストデータ)「アルバムを保存する」(アルバムファイルとして纏めて保存)ということになるでしょうか?
(3)↑の(1)(2)から、写真は「フルパスファイル名」+「画像データ」+「コメントテキストデータ」を単位とするクラス、アルバムクラスは「台紙を「SDIウィンドウ」+「仮想ウィンドウ」を前提に、GDI+を使った各種「表示機能」と写真クラスのオブジェクトの管理機能を持たせればよい、と思いました。

当時はまだGDI+をよく分かっていなかったので、先ずはこのお勉強を始め、その成果を「【GDI+】ブログ」に書き記したこと、ご存じの通りです。
次回はアルバムソフトの最小単位である「写真(CPHOTO)クラス」から始めます。

 

年が改まってから、DirectShowを使った、動画、音楽再生ソフトのDirectShowを開発し、その後何となく見つけたGDI+に興味を持って、色々と学習しながら試行錯誤で開発してきたAlbumが一応の完成を見ました。

 

 

これから少しの間、その開発に関わるお話を一人語りで進めようと思っています。乞御期待。

 

アルバムソフトを作っていて、既に公開しているサービスクラスのCEXTCHKクラス

//フルパスファイル名の拡張子チェック 
bool CEXTCHK::CheckExt(char* filename, char* flt)

を改良することにしました。

 

GDI+を使うと次のエンコーダーが使えるようになります。(更にデコーダーはwmf、emf、icoがある。)従って「ファイルを開く」ダイアログではフィルターを次のようにしています。(注)

//イメージファイルのフィルター
#define        IMGFILTER    "イメージファイル\0*.bmp;*.jpeg;*.jpg;*.gif;*.tiff;*.png;*.wmf;*.emf;*.ico\0\0"

注:これによると、jpegファイルは*.jpegと*.jpgの二つの表記があるようなので、二つ書いています。

一方、GDI+のエンコーダーは、例えば GetImageEncoders関数で使う ImageCodecInfoクラスのメンバーMimeType(CODECのMIME(Multipurpose Internet Mail Extensions)型)ではワイド文字(WCHAR)で、次の配列のような文字列になっています。

//エンコーダーリスト
const WCHAR* g_EncoderList[5] = {L"image/bmp", L"image/jpeg", L"image/gif", L"image/tiff", L"image/png"};

のように規定されています。

 

こんなことから、当初のCheckExt関数ではbool型(TRUE == !0かFALSE == 0)ヲ返していたのですが、せっかく拡張子をチェックしているので、今までのboolを返していた時と同じ効果を保ちながら、該当した拡張子の番号を返すようにしてみました。

 

【従来のCheckExt関数】

//フルパスファイル名の拡張子チェック 
bool CEXTCHK::CheckExt(char* filename, char* flt) {

    char** ext;
    int n = GetExt(flt, ext);
    //小文字チェック
    for(int i = 0; i < n; i++) {
        if(strstr(filename, ext[i])) {
            return TRUE;
        }
    }
    ConvertUC();
    //大文字チェック
    for(int i = 0; i < n; i++) {
        if(strstr(filename, ext[i])) {
            return TRUE;
        }
    }
    return FALSE;
}
 

【改良後のCheckExt関数】

//フルパスファイル名の拡張子チェック(1~ベースの拡張子番号を返す)
int CEXTCHK::CheckExt(char* filename, char* flt) {

    char** ext;
    int n = GetExt(flt, ext);
    //小文字チェック
    for(int i = 0; i < n; i++) {
        if(strstr(filename, ext[i])) {
            return i + 1;
        }
    }
    ConvertUC();
    //大文字チェック
    for(int i = 0; i < n; i++) {
        if(strstr(filename, ext[i])) {
            return i + 1;
        }
    }
    return NULL;
}
 

こうすることで、CheckExt関数の戻り値が「見つからなければ0(== FALSE)」、「見つかれば1から始まる配列番号(== !0 == TRUE)」、具体的には↑のフィルターを使った場合、

.bmp → 1

.jpeg → 2

.jpg → 3

.gif → 4

.tiff → 5

.png → 6

となり、従来のCheckExt関数を条件式で使えるとともに、上記したようなワイド文字列配列と関連付けられるようになりました。

次のアップロードからBCCSkeltonのCEXTCHK.hを変えておきます。