2024/01/15(月) [n年前の日記]
#1 [prog] C/C++で設定ダイアログを表示してみたい
C/C++ と OpenGL を使ってWindows用のスクリーンセーバを作りたい。えてしてWindows用のスクリーンセーバは、設定ダイアログを表示して、動作を多少変更することができたりする。
そんなわけで、C/C++ を使って設定ダイアログを表示する方法について調べていた。
一般的なスクリーンセーバの場合、設定ダイアログ上でGUI部品(コントロール、ウィジェットとも呼ばれる)を配置するには、リソースファイル(拡張子が .rc)を使う。
_ダイアログ(Win32API)(C言語) - 超初心者向けプログラミング入門
また、設定ダイアログ上のGUI部品を操作するための関数も用意されていた。
一応、Visual Studio Community 2019 上で試したところ、それらしく動くプログラムを作れたのだけど…。これを MinGW (gcc 6.3.0) で再現しようとしたら、リソースコンパイラ windres.exe が syntax error を吐いて、なかなか上手く行かない…。windres.exe では、msctls_trackbar32 (trackbar、slider) は指定できないのだろうか…? ググっても情報が全く出てこないし…。
そんなわけで、C/C++ を使って設定ダイアログを表示する方法について調べていた。
- C/C++で、何をどうすれば設定ダイアログに見えるダイアログを表示できるのか。
- 設定ダイアログ上の部品レイアウトはどうやって行うのか。
- 設定ダイアログ上の部品の値を読み取ったり、値を設定するにはどうしたらいいのか。
一般的なスクリーンセーバの場合、設定ダイアログ上でGUI部品(コントロール、ウィジェットとも呼ばれる)を配置するには、リソースファイル(拡張子が .rc)を使う。
- リソースファイルはテキスト形式で記述する。
- リソースファイルをリソースコンパイラに渡して、オブジェクトファイル(.o)に変換して、実行ファイル(.exe)にリンクする。
_ダイアログ(Win32API)(C言語) - 超初心者向けプログラミング入門
また、設定ダイアログ上のGUI部品を操作するための関数も用意されていた。
一応、Visual Studio Community 2019 上で試したところ、それらしく動くプログラムを作れたのだけど…。これを MinGW (gcc 6.3.0) で再現しようとしたら、リソースコンパイラ windres.exe が syntax error を吐いて、なかなか上手く行かない…。windres.exe では、msctls_trackbar32 (trackbar、slider) は指定できないのだろうか…? ググっても情報が全く出てこないし…。
◎ Visual Studio版 :
とりあえず、Visual Studio Community 2019上で動作したソースを載せておく。
どういう動作をするサンプルかというと…。
_dialogtest01.cpp
_resource.h
_dialogtest01.rc
_dialogtest01.h
_framework.h
ビルドして実行すると以下のような見た目になる。
どういう動作をするサンプルかというと…。
- メインウインドウ上の "Open Dialog" ボタンを押すと、ダイアログが開く。(リソースファイルで定義したダイアログを開けている)
- ダイアログ上で値を変更して "OK" ボタンを押すと、メインウインドウ上の値の表示も変わる。(ダイアログ上で設定した値を読み取れている。)
_dialogtest01.cpp
#include <Windows.h>
#include <wchar.h>
#include <stdlib.h>
#include <tchar.h>
#include <CommCtrl.h>
#include "framework.h"
#include "DialogTest01.h"
#define MAX_LOADSTRING 100
#define IDC_BUTTON_OPENDLG 110
// global work
HINSTANCE hInst;
WCHAR szTitle[MAX_LOADSTRING]; // title bar text
WCHAR szWindowClass[MAX_LOADSTRING]; // main window class name
static WCHAR txt[1024]; // message work
// global paramater work
static int waitValue = 15;
static int speedValue = 1000;
static int numberValue = 1000;
static int fps_display = 1;
// prototype
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);
// entry point
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// init global sstring
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_DIALOGTEST01, szWindowClass, MAX_LOADSTRING);
// Register window class
{
WNDCLASSEXW wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_DIALOGTEST01));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = nullptr; // set menu
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
RegisterClassExW(&wcex);
}
// init application. create main window
{
hInst = hInstance;
HWND hWnd = CreateWindowW(
szWindowClass,
szTitle,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, // x, y
320, 240, // width, height
nullptr,
nullptr,
hInstance,
nullptr
);
if (!hWnd) return FALSE;
ShowWindow(hWnd, nCmdShow); // display main window
UpdateWindow(hWnd);
}
MSG msg;
// main message loop
while (GetMessage(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
// main window procedure
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
// static WCHAR txt[1024];
switch (message)
{
case WM_CREATE:
// create main window
// create "Open Dialog" button
CreateWindow(
L"BUTTON", L"Open Dialog",
WS_CHILD | WS_VISIBLE,
20, 20, 100, 24, // x, y, width, height
hWnd, (HMENU)IDC_BUTTON_OPENDLG, hInst, NULL
);
break;
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
switch (wmId)
{
case IDC_BUTTON_OPENDLG:
{
// push "Open Dialog" button
// Open dialog
int ret;
ret = DialogBox(hInst, MAKEINTRESOURCE(IDD_DIALOG1), hWnd, DlgProc);
// dialog results
switch (ret)
{
case IDOK:
// push "OK" button
wcscpy_s(txt, L"IDOK"); // set text message
break;
case IDCANCEL:
// push "Cancel" button
wcscpy_s(txt, L"IDCANCEL"); // set text message
break;
}
InvalidateRect(hWnd, NULL, TRUE);
}
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
// draw main window
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// draw message
TextOut(hdc, 20, 70, txt, lstrlen(txt));
// draw paramater value
{
WCHAR s[256];
swprintf_s(s, L"wait=%d", waitValue);
TextOut(hdc, 20, 90, s, lstrlen(s));
swprintf_s(s, L"speed=%d", speedValue);
TextOut(hdc, 20, 110, s, lstrlen(s));
swprintf_s(s, L"number=%d", numberValue);
TextOut(hdc, 20, 130, s, lstrlen(s));
swprintf_s(s, L"fps_display=%d", fps_display);
TextOut(hdc, 20, 150, s, lstrlen(s));
}
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
// destroy main window
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// dialog procedure
INT_PTR CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) {
case WM_INITDIALOG:
{
// initialize dialog
HWND cHwnd;
// wait slider (trackbar)
cHwnd = GetDlgItem(hDlg, IDC_SLIDER1); // get hundle
SendMessage(cHwnd, TBM_SETRANGE, TRUE, MAKELPARAM(5, 200)); // set range
SendMessage(cHwnd, TBM_SETTICFREQ, 5, 0); // set tick
SendMessage(cHwnd, TBM_SETPOS, TRUE, (LPARAM)waitValue); // set position
// speed slider
cHwnd = GetDlgItem(hDlg, IDC_SLIDER2);
SendMessage(cHwnd, TBM_SETRANGE, TRUE, MAKELPARAM(100, 4000));
SendMessage(cHwnd, TBM_SETTICFREQ, 100, 0);
SendMessage(cHwnd, TBM_SETPOS, TRUE, (LPARAM)speedValue);
// number slider
cHwnd = GetDlgItem(hDlg, IDC_SLIDER3);
SendMessage(cHwnd, TBM_SETRANGE, TRUE, MAKELPARAM(10, 4000));
SendMessage(cHwnd, TBM_SETTICFREQ, 100, 0);
SendMessage(cHwnd, TBM_SETPOS, TRUE, (LPARAM)numberValue);
// init checkbox
SendMessage(GetDlgItem(hDlg, IDC_CHECK1), BM_SETCHECK,
((fps_display == 0) ? BST_UNCHECKED : BST_CHECKED), 0);
}
return (INT_PTR)TRUE;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDOK:
// push "OK" button
// get paramater and set global work
waitValue = SendMessage(GetDlgItem(hDlg, IDC_SLIDER1), TBM_GETPOS, NULL, NULL);
speedValue = SendMessage(GetDlgItem(hDlg, IDC_SLIDER2), TBM_GETPOS, NULL, NULL);
numberValue = SendMessage(GetDlgItem(hDlg, IDC_SLIDER3), TBM_GETPOS, NULL, NULL);
if (SendMessage(GetDlgItem(hDlg, IDC_CHECK1), BM_GETCHECK, 0, 0) == BST_UNCHECKED)
fps_display = 0;
else
fps_display = 1;
EndDialog(hDlg, IDOK);
return (INT_PTR)TRUE;
case IDCANCEL:
// push "Cancel" button
EndDialog(hDlg, IDCANCEL);
return (INT_PTR)TRUE;
case IDC_BUTTON_RESET:
{
// push "Reset" button
// change status text
SetWindowText(GetDlgItem(hDlg, IDC_STATUS1), TEXT("Reset"));
// set slider position
SendMessage(GetDlgItem(hDlg, IDC_SLIDER1), TBM_SETPOS, TRUE, 15);
SendMessage(GetDlgItem(hDlg, IDC_SLIDER2), TBM_SETPOS, TRUE, 1000);
SendMessage(GetDlgItem(hDlg, IDC_SLIDER3), TBM_SETPOS, TRUE, 1000);
// change checkbox
SendMessage(GetDlgItem(hDlg, IDC_CHECK1), BM_SETCHECK, BST_CHECKED, 0);
}
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}
_resource.h
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ で生成されたインクルード ファイル。
// DialogTest01.rc で使用
//
#define IDC_MYICON 2
#define IDD_DIALOGTEST01_DIALOG 102
#define IDS_APP_TITLE 103
#define IDI_DIALOGTEST01 104
#define IDI_SMALL 105
#define IDC_DIALOGTEST01 106
#define IDR_MAINFRAME 128
#define IDD_DIALOG1 129
#define IDC_CHECK1 1000
#define IDC_BUTTON_RESET 1001
#define IDC_SLIDER1 1002
#define IDC_SLIDER2 1003
#define IDC_SLIDER3 1004
#define IDC_STATUS1 1005
#define IDC_STATIC -1
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NO_MFC 1
#define _APS_NEXT_RESOURCE_VALUE 130
#define _APS_NEXT_COMMAND_VALUE 32771
#define _APS_NEXT_CONTROL_VALUE 1007
#define _APS_NEXT_SYMED_VALUE 110
#endif
#endif
_dialogtest01.rc
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#ifndef APSTUDIO_INVOKED
#include "targetver.h"
#endif
#define APSTUDIO_HIDDEN_SYMBOLS
#include "windows.h"
#undef APSTUDIO_HIDDEN_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// 日本語 (日本) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_JPN)
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_DIALOGTEST01 ICON "DialogTest01.ico"
IDI_SMALL ICON "small.ico"
/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//
IDD_DIALOG1 DIALOGEX 0, 0, 289, 140
STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Dialog"
FONT 11, "Segoe UI", 400, 0, 0x0
BEGIN
DEFPUSHBUTTON "&OK",IDOK,168,96,50,14
PUSHBUTTON "&Cancel",IDCANCEL,228,96,50,14
PUSHBUTTON "Reset",IDC_BUTTON_RESET,84,96,50,14
CONTROL "",IDC_SLIDER1,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,96,24,180,15
CONTROL "",IDC_SLIDER2,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,96,48,180,15
CONTROL "",IDC_SLIDER3,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,96,72,180,15
CONTROL "FPS display",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,96,53,10
CTEXT "ssstars GL setting",IDC_STATIC,7,7,275,8
LTEXT "Wait (5 - 200msec)",IDC_STATIC,12,24,63,8
LTEXT "Speed (100 - 4000)",IDC_STATIC,12,48,64,8
LTEXT "Number (10 - 4000)",IDC_STATIC,12,72,66,8
LTEXT "Status",IDC_STATUS1,7,120,275,13,SS_SUNKEN | WS_BORDER
END
/////////////////////////////////////////////////////////////////////////////
//
// DESIGNINFO
//
#ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO
BEGIN
IDD_ABOUTBOX, DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 162
TOPMARGIN, 7
BOTTOMMARGIN, 55
END
IDD_DIALOG1, DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 282
TOPMARGIN, 7
BOTTOMMARGIN, 133
END
END
#endif // APSTUDIO_INVOKED
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#ifndef APSTUDIO_INVOKED\r\n"
"#include ""targetver.h""\r\n"
"#endif\r\n"
"#define APSTUDIO_HIDDEN_SYMBOLS\r\n"
"#include ""windows.h""\r\n"
"#undef APSTUDIO_HIDDEN_SYMBOLS\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// String Table
//
STRINGTABLE
BEGIN
IDS_APP_TITLE "DialogTest01"
IDC_DIALOGTEST01 "DIALOGTEST01"
END
#endif // 日本語 (日本) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED
_dialogtest01.h
_framework.h
ビルドして実行すると以下のような見た目になる。
◎ 分かったことをメモ :
試してみて分かったことをメモ。
リソースファイルの記述内容に基づいてダイアログを表示するには、DialogBox() を使う。
_第13章 ダイアログボックス
_DialogBox関数でリソースからモーダルダイアログを作る | ぬの部屋(仮)
DialogBox() には、リソースファイル内でダイアログ関係を定義してある部分のID(今回のソースなら、IDD_DIALOG1)と、ダイアログプロシージャを指定して呼び出す。これだけでダイアログが表示される。
ダイアログプロシージャ(今回は DlgProc() が担当)は、ダイアログ上でユーザが何か操作をしたときに呼ばれる関数、らしい。たぶん。
ダイアログプロシージャが呼ばれた際は、ダイアログが今どんな状態なのかをメッセージとして送ってくる。
ダイアログ上のGUI部品の値を設定したり、値を読み取ったりするときは、SendMessage() その他を使う。get したいのか、set したいのかを指定することで、値を読んだり、値を変更したりできる。また、SendMessage() 以外にも、特定の種類のGUI部品に対して簡単に操作できる関数が用意されていたりもする。(EDITTEXT に対する GetDlgItemText(), SetDlgItemText() とか。)
SendMessage() には、どのGUI部品を対象にしたいのか、ハンドルなるものを渡して教えてやらないといけない。 GUI部品のハンドルは、GetDlgItem() で取得できる。
_リソースから作成したダイアログボックスのコントロールへアクセスする | ぬの部屋(仮)
ダイアログ上にtrackbar(slide) を出したい時は、"msctls_trackbar32" を指定してやる。コレを使う時は、.c/.cpp 内で、commctrl.h を include してやる必要がある。そうしないと、TBM_* というシンボルが使えない。
_トラックバー - インコのWindowsSDK
リソースファイルの記述内容に基づいてダイアログを表示するには、DialogBox() を使う。
_第13章 ダイアログボックス
_DialogBox関数でリソースからモーダルダイアログを作る | ぬの部屋(仮)
DialogBox() には、リソースファイル内でダイアログ関係を定義してある部分のID(今回のソースなら、IDD_DIALOG1)と、ダイアログプロシージャを指定して呼び出す。これだけでダイアログが表示される。
ダイアログプロシージャ(今回は DlgProc() が担当)は、ダイアログ上でユーザが何か操作をしたときに呼ばれる関数、らしい。たぶん。
ダイアログプロシージャが呼ばれた際は、ダイアログが今どんな状態なのかをメッセージとして送ってくる。
- WM_INITDIALOG ... ダイアログが表示された際に一度だけ呼ばれる。ダイアログ上の部品の値等を初期化したい時はここで行う。
- WM_COMMAND ... ボタン等が押された時に呼ばれる。一緒に送られてきたIDを見て、どのボタンが押されたのかを判別して処理を書ける。
ダイアログ上のGUI部品の値を設定したり、値を読み取ったりするときは、SendMessage() その他を使う。get したいのか、set したいのかを指定することで、値を読んだり、値を変更したりできる。また、SendMessage() 以外にも、特定の種類のGUI部品に対して簡単に操作できる関数が用意されていたりもする。(EDITTEXT に対する GetDlgItemText(), SetDlgItemText() とか。)
SendMessage() には、どのGUI部品を対象にしたいのか、ハンドルなるものを渡して教えてやらないといけない。 GUI部品のハンドルは、GetDlgItem() で取得できる。
_リソースから作成したダイアログボックスのコントロールへアクセスする | ぬの部屋(仮)
ダイアログ上にtrackbar(slide) を出したい時は、"msctls_trackbar32" を指定してやる。コレを使う時は、.c/.cpp 内で、commctrl.h を include してやる必要がある。そうしないと、TBM_* というシンボルが使えない。
_トラックバー - インコのWindowsSDK
◎ MinGW版でハマってる :
ということで、Visual Studio上では動いてくれたけど、MinGW (gcc 6.3.0) では上手く行かない…。リソースコンパイラ windres.exe が syntax error を出す。
渡してる resource.rc は以下。
以下のあたりでエラーを出している。これは trackbar(slider?) を定義してる。"msctls_trackbar32" というのが、trackbar のはずだけど…。
もしかすると、windres.exe は、このコントロール(GUI部品、ウィジェット)に未対応だったりするのだろうか…? 代わりに EDITTEXT を記述すると、あっさりビルドが通るし…。
渡してる resource.rc は以下。
/* resource */ #include <windows.h> #include <commctrl.h> #include "resource.h" /* Icon */ IDI_APP_ICON ICON "appli.ico" IDI_SMALL ICON "small.ico" /* Dialog */ IDD_DIALOG1 DIALOG DISCARDABLE 0, 0, 289, 140 STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Dialog" FONT 11, "Segoe UI" BEGIN DEFPUSHBUTTON "OK", IDOK, 168, 96, 50, 14 PUSHBUTTON "Cancel", IDCANCEL, 228, 96, 50, 14 PUSHBUTTON "Reset", IDC_RESET, 10, 100, 50, 14 AUTOCHECKBOX "FPS display", IDC_FPSDISPLAY, 12, 96, 53, 10 CTEXT "ssstars GL setting", IDC_STATIC, 7,7,275,8 LTEXT "Wait (5 - 200msec)", IDC_STATIC, 12,24,63,8 LTEXT "Speed (100 - 4000)", IDC_STATIC, 12,48,64,8 LTEXT "Number (10 - 4000)", IDC_STATIC, 12,72,66,8 LTEXT "Status", IDC_STATUS1, 7,120,275,13,SS_SUNKEN | WS_BORDER CONTROL "Wait", IDC_WAITVALUE, "msctls_trackbar32", TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP, 96,24,180,15 CONTROL "Speed", IDC_SPEED, "msctls_trackbar32", TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP, 96,48,180,15 CONTROL "Number", IDC_NUMBER, "msctls_trackbar32", TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP, 96,72,180,15 END
以下のあたりでエラーを出している。これは trackbar(slider?) を定義してる。"msctls_trackbar32" というのが、trackbar のはずだけど…。
CONTROL "Wait", IDC_WAITVALUE, "msctls_trackbar32", TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP, 96,24,180,15 CONTROL "Speed", IDC_SPEED, "msctls_trackbar32", TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP, 96,48,180,15 CONTROL "Number", IDC_NUMBER, "msctls_trackbar32", TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP, 96,72,180,15
もしかすると、windres.exe は、このコントロール(GUI部品、ウィジェット)に未対応だったりするのだろうか…? 代わりに EDITTEXT を記述すると、あっさりビルドが通るし…。
[ ツッコむ ]
以上です。
