Delphi Programming / Object Pascal
[掲載 2013年03月31日] [更新 2017年10月18日] Delphi サンプルプログラム集 |
889_VCL スタイルをリソースとして埋め込んで使用 |
動作確認等 |
Windows 7 U64(SP1) + Delphi XE2 Pro VCL-32
DLL 内から文字列を渡すサンプル以外は Delphi XE2 以降専用 |
|
889_VCLStyleResource.zip [7,580 KB] 2017年04月26日版 (EXE 同梱) |
- 2016年01月09日
- VCL スタイルの適用に関する注意を追加
- 実行時の VCL スタイルの変更のサンプルと,その関係記事を追加
|
Delphi XE2 では,VCLスタイルを使用して,フォームやコントロールのデザインを変更することができるようになりました.[プロジェクト] [オプション] [アプリケーション] [表示] で設定します.
本ページのサンプルは,IDE で VCL スタイルを設定するのではなく,リソースとして EXE に VCL スタイルを埋め込んでおきます.それを実行時に取り出して設定します.
DLL 本来の機能とは言えませんが,例えば,DLL 内でフォームを表示する際,VCL スタイルを適用したい場合に利用できます.DLL のプロジェクトでは,設計時に VCL スタイルが設定できません. |
[備考 1]
アプリケーション起動時以外に VCL スタイルを適用する場合は注意が必要です.
VCL スタイルを適用すると,コントロールが再生成される事があります.そのため,コントロールのハンドル値が変わり,access violation (AV - アクセス違反) が発生する場合があります.
本ページのサンプルでは,その現象を避けるため,プロジェクトのソースコードで VCL スタイルの設定を行っています.フォームを実装した DLL では,DLL のプロセス生成時に VCL スタイルの設定を実行しています.
|
[備考 2]
Delphi XE3 以降のビジュアルコンポーネントには,StyleElements プロパティが実装されています.このプロパティの値を [] (空) にすると,スタイルの適用から除外することができます.
例えば,フォームだけに VCL スタイルを適用するということも可能になっています.
|
01_VCL スタイルを EXE のリソースとして埋め込んで使用 |
[プロジェクト] から [リソースと画像] を選択します.下図のダイアログで,[追加] ボタンをクリックして,追加する VCL スタイルのファイルを選択します.vsf ファイルは,ファイルの選択ダイアログで,[全てのファイル] にしないとリストに現れません.
選択したら,リソースの識別子を決めます.これは任意の英数字の名前にできますが,空白は使えません.リソースタイプは VCLSTYLE にします.これはリストにないのでタイプします. 入力したら,[OK] ボタンで閉じます. |
|
図1
リソースを追加
- [追加] でファイルを選択
- [リソース識別子] は任意の英数字の文字列
- [リソースタイプ] は VCLSTYLE とする
|
次ような,拡張子が .rc のリソースの定義ファイルができます.
古いバージョンの Delphi では,リソースを埋め込むには,このような内容のテキストファイルを手動で作成しなければなりませんでしたが,Delphi 2009 以降では,ダイアログを表示して設定できるようになっています. |
Resource_1 VCLSTYLE "AmethystKamri.vsf" |
メニューの [プロジェクト] で [ソースの表示] を選択し,プロジェクトのソースコードを以下のようにします.
VCL スタイルはプロジェクト単位の設定です.そこで,アプリケーション開始時に実行するコードとするために,ここに書くことにしました.
このコードは,該当リソースがない場合は何も設定をしません.また,すでにリソースが読み込まれている場合も何もしないようにしています.実際には,EXE 実行時に実行するコードなので,この判定は不要ですが,フォーム生成時等にリソースから読み出す時には参考になるでしょう. |
リスト1
プロジェクトの [ソース]
uses に Vcl.Thems と Vcl.Styles が必要 |
program Project1;
{$R *.dres}
uses
Forms,
Vcl.Themes,
Vcl.Styles,
Unit1 in 'Unit1.pas' {Form1};
{$R *.res}
//-----------------------------------------------------------------------------
// VCLスタイルを設定する関数
//-----------------------------------------------------------------------------
function SetVCLStyle: Boolean;
var
VCLH: TStyleManager.TStyleServicesHandle;
begin
Result := False;
if FindResource(HInstance, 'Resource_1', 'VCLSTYLE') <> 0 then begin
VCLH := TStyleManager.LoadFromResource(HInstance, 'Resource_1', 'VCLSTYLE');
if VCLH <> nil then begin
TStyleManager.SetStyle(VCLH);
Result := True;
end;
end;
end;
begin
//VCLスタイルを設定
SetVCLStyle;
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
テスト用のフォームの設計時の画面と実行結果です.
フォームのユニットには,コードの追加はありません.コンポーネント類は配置しているだけです. |
|
図2
設計時画面
- コントロール類 (コンポーネント類) は配置しているだけで,コードはなにもない
|
|
図3
実行画面 |
<0br>
02_DLL の場合 - DLL にフォームを追加 |
次に,DLL 内で表示するフォームでテストしてみます.
DLL 内でフォームを表示可能にする DLL は次のように作成しました.
まず,[ファイル] [新規作成] で [その他] を選択します.
下図のダイアログで [ダイナミックリンクライブラリ] を選択して [OK] で閉じます. |
|
図4
|
次にフォームを追加します.
[ファイル] [新規作成] で [VCL フォーム] を選択します.下図のダイアログでは,[はい] をクリックします.これで,この DLL のプロジェクトにフォームが 1 つ追加されました.
今回は,DLL のプロジェクト名を DLLTest_DLL とし.フォームの名前は DllForm,フォームのユニット名を DllFormUnit.pas として保存しました.
既存のフォームは [プロジェクト] [プロジェクトに追加] で追加します. |
|
図5
|
03_DLL の場合 - DllProc を使用した DLL のコード |
VCL スタイルはプロジェクト単位です.ここでは,DLL がロードされた時 (DLL プロセス生成時) に適用するようにしています.
DLL のプロジェクトとフォーム等を保存したら,上のサンプルの時と同じように,VCL スタイルのファイルをリソースとして追加します.
DLL の場合,[プロジェクト] [オプション] では VCL スタイルを選択できません.したがって,リソースとして埋め込むか,ファイルから読み込むしかありません.
DLL のプロジェクトのソースのコードは,次のようにしました.
DllProc のフラグについては,MSDN の DllMain の記事を参考にしてください.2013年04月01日現在のところ,Delphi のオンラインヘルプの整備されていないようです.また,Application と
Screen 変数を退避し,DLL 使用終了時に元の値に戻すようにしています.ただし,本サンプルには,これらの変数を使用したコードはありません.
ShowTestForm
は,この DLL を使用するプログラムから実行する DLL 内の関数です.このサンプルではこの関数だけなので,dpr 内に処理コードを書いています.関数が多い場合は,専用のユニットとした方がいいでしょう.
DLL に追加したフォームのユニットには,何もコードを追加していません. |
[備考]
IDE で,プロジェクトグループに DLL のプロジェクトと,その DLL を使用するプロジェクトを追加して開発ができますが,1 つのプロジェクトは 1 つのプログラム単位です.IDE 上で同時にデバックできるからと言って,DLL 内のコードを,他のユニット内のコードを利用するような方法では利用できません.
つまり,DLL は別のアプリケーションと考えます.例えば,Application 変数の値は,そのアプリケーション固有の値です.
|
library DllTest_DLL;
{$R *.dres}
uses
Winapi.Windows,
Vcl.Dialogs,
System.Classes,
Vcl.Themes,
VCL.Styles,
Vcl.Controls,
Vcl.Forms,
DllFormUnit in 'DllFormUnit.pas' {DllForm};
{$R *.res}
var
SaveDllProc : procedure(Reason: Integer);
SaveApplication : TApplication;
SaveScreen : TScreen;
//-----------------------------------------------------------------------------
// DllProc関数の処理
// 本サンプルでは,DllEntryという名前に置き換えている
//-----------------------------------------------------------------------------
procedure DLLEntry(Reason: Integer);
var
VclH : TStyleManager.TStyleServicesHandle;
begin
if Reason = DLL_PROCESS_ATTACH then begin
//元のTApplicationとTScreenを退避
//本サンプルでは必要ないが参考用として
SaveApplication := Application;
SaveScreen := Screen;
//DLLのプロセス生成時の処理
//リソースからVCLスタイルの情報を取得してセット
if FindResource(HInstance, 'Resource_2', 'VCLSTYLE') <> 0 then begin
if TStyleManager.TryLoadFromResource(HInstance,
'Resource_2',
'VCLSTYLE',
VclH) then begin
if VclH <> nil then TStyleManager.SetStyle(VclH);
end;
end;
end else
if Reason = DLL_PROCESS_DETACH then begin
//退避したTApplicationとTScreen変数を元に戻す
Application := SaveApplication;
Screen := SaveScreen;
//その他のDLLプロセス終了時の処理
end;
//元のDllProcを実行
if Assigned(SaveDllProc) then begin
if Reason >= 0 then begin
SaveDllProc(Reason);
end;
end;
end;
//-----------------------------------------------------------------------------
// 外部から呼び出して実行する関数
// DllFormというフォームをモーダル表示
// 引数に文字列を追加する場合は,PCharにする
//-----------------------------------------------------------------------------
function ShowTestForm(hOwner: HWND): TModalResult; stdcall;
var
ARect : TRect;
begin
GetWindowRect(hOwner, ARect);
//この代入のコードは,TDllForm.Createの引数をnilにすれば必要ないが参考として
Application.Handle := hOwner;
DllForm := TDllForm.Create(Application);
try
//表示位置を変更できるようpoDesignedにして,表示位置を変更
DllForm.Position := poDesigned;
DllForm.Left := ARect.Left + 100;
DllForm.Top := ARect.Top + 50;
Result := DllForm.ShowModal;
finally
DllForm.Free;
end;
Application.Handle := 0;
end;
//-----------------------------------------------------------------------------
exports
ShowTestForm;
//-----------------------------------------------------------------------------
// DLL初期化時の処理
// DllProcはDLLのプロセス,スレッドの開始.終了時に自動実行される(SysInit.pas)
//-----------------------------------------------------------------------------
begin
//元のDllProcを退避
SaveDllProc := @DllProc;
DllProc := @DLLEntry;
//DLL初期化時に1回だけ実行
DLLEntry(DLL_PROCESS_ATTACH);
end.
04_DLL の場合 - DLL を使用する側のテストプログラム |
DLL を使用する側のプロジェクトを作成します.
今回は,下図のように DLL のプロジェクトと,DLL を使用する側のプロジェクトのファイルを同じフォルダに置いています. |
|
図6
ファイル構成
- AmethystKamri.vsf は VCL スタイルのファイル
スタイル名は Amethyst Kamri
- DllTest_DLL.dpr が DLL のプロジェクトファイル
- Project1.dpr は DLL を利用する側のプロジェクト
- DllTest_DLLResource.rc は VCL スタイルのリソース定義
|
フォームには,ボタンを 1 つ追加して,以下のコードとしています.
このコードでは,DLL が必ず存在するものとして,静的に DLL を使用していますが,動的な利用の方が確実です. |
|
図7
設計時画面 |
|
図8
EXE 起動時の画面 |
|
図9
[Button1] をクリック
- DLL 内部でフォームを生成して表示
- そのフォームに VCL スタイルを適用
|
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private 宣言 }
public
{ Public 宣言 }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function ShowTestForm(hOwner: HWND): TModalResult; stdcall; external 'DllTest_DLL.dll';
//-----------------------------------------------------------------------------
// DLL内のShowTestForm関数を実行する
//-----------------------------------------------------------------------------
procedure TForm1.Button1Click(Sender: TObject);
begin
ShowTestForm(Application.Handle);
end;
end.
05_DLL の場合 - 実行時の VCL スタイルの変更 |
DLL 内で表示したフォームの VCL スタイルを変更するには,例えば DLL に下のような関数を追加して,この関数を呼び出し側で実行します.
当然ですが,DLL 内で表示するフォームは Show メソッドを使用して,モードレス表示にする必要があります.そうしないと,呼び出し側で操作できません.
本サンプルでは,DLL のフォームを表示している時に,呼出し側のプロジェクトの VCL スタイルを変更すると,DLL のフォームは非表示になります.これは,コントロールの再生成により,コントロールのハンドルの値が変わってしまうためです.
これを回避するには,表示中の DLL 内のフォームを破棄してから,再度フォームを生成して表示する必要があります.あるいは,DLL を解放し,再ロードしてから DLL のフォームを表示します.そのためには,DLL を動的にロードする方式にします.
|
|
図10
呼出し側と DLL の VCL スタイルを同時に変更しようとすると現れるダイアログの例
このダイアログが現れるかどうかは,コーディングによる |
//-----------------------------------------------------------------------------
// 外部から呼び出して実行する関数
// VCLのスタイルを変更する
// AStyleはリソース名
//-----------------------------------------------------------------------------
function ChangeVCLStyle(AStyle: PChar): Boolean; stdcall;
var
VclH : TStyleManager.TStyleServicesHandle;
begin
Result := False;
//問題が発生する可能性があるので,VCLスタイルが適用されていない場合は変更しない
//DLL生成時に何かしらのVCLスタイルを適用しておくこと
if not TStyleManager.IsCustomStyleActive then exit;
//リソースからVCLスタイルの情報を取得してセット
if FindResource(HInstance, AStyle, 'VCLSTYLE') <> 0 then begin
if TStyleManager.TryLoadFromResource(HInstance,
AStyle,
'VCLSTYLE',
VclH) then begin
if VclH <> nil then begin
TStyleManager.SetStyle(VclH);
Result := True;
end;
end;
end;
end;
上の関数を実行する例です.Resource_4 はリソース識別子の名前です. |
procedure TForm1.Button2Click(Sender: TObject);
begin
ChangeVCLStyle('Resource_4');
end;
下図は,添付のサンプルを実行した結果です.
呼出し側のプログラムでは,設定時に VCL スタイルを設定しています.[TEST] は,アプリを終了する項目だけを実装した TMainMenu です.[TEST] をクリックするとメニューを表示します.
|
|
図11
EXE を実行して Button1 クリック |
|
図12
Button2 をクリック |
|
図13
メニューの [Close] でアプリ終了 |
06_呼出し側と DLL の両方に VCL スタイルを適用する場合 |
上のサンプルでは,呼び出し側のフォームにも VCL スタイルを適用しています.上のサンプルプログラムは,Delphi XE6, XE7, XE8, Delphi 10 Seattle では正常に動作しません.
Delphi XE6 以降では,TMainMenu, TPopupMenu 等のメニュー (TMenuItem) に対して操作すると,下図のように,アクセス違反等が発生します.これは,呼び出し側のプロジェクトと DLL の両方に VCL スタイルを適用している場合に発生します.上のサンプルよりも前のサンプルのように,VCL スタイルを片方だけに適用する場合は発生しないようです.
筆者は,Delphi XE6, XE8 については確認していませんが,ネット上に情報がありました.
|
|
図14
Delphi XE6 以降で発生するエラー
- エラーや例外の内容は,コーディングや実行時の操作によって異なる
|
もし,Delphi XE6 以降で TMenuItem を使用するのであれば,以下のコードを,呼出し側のアプリの初期化の時,または,DLL のプロセス生成時に実行して,TMenuItem に対して VCL スタイルフックを無効にします.現在のところ,この方法しかないようです.
TSysPopupStyleHook の使用には,uses に Vcl.SysStyles が必要です.Vcl.SysStyles は,Delphi XE5 以前にはありません.
|
TCustomStyleEngine.UnRegisterSysStyleHook('#32768', TSysPopupStyleHook);
07_DLL 内で MDI アプリの子ウィンドウを生成 |
この記事を書く元となったのは,Delphi のコミュニティの場での質問でした.
DLL 内で MDI の 子ウィンドウを作成する際に,子ウィンドウにも親と同じ VCL スタイルを適用したいというものです.
DLL 内の Application 変数と Screen
は独立しています.異なるプロジェクトでは一般的にそうなります.そこで,DLL 内で生成するウィンドウ,つまりフォームを子ウィンドウにするには,
- DLL 内の Application と Screen の値を呼び出し側の値と一致させる
- 呼び出し側のフォームの FormStyle の値を fsMDIFormとする
- DLL 内で生成するフォームの FormStyle の値を fsMDIChild とする
という処理が必要です.
DLL を使用したこの構成が,どのような場合に必要なのかは分かりませんが,上のサンプルを,図の下に示すコードに変更してテストした結果です.
EXE も DLL も元々別のプロジェクトです,DLL 本来の意味としては,別のアプリケーションとして考えてもいいかも知れません.
DLL と,その DLL を使用する EXE とで情報のやりとりを行うには,メッセージを使用する方法も考えられますが,かなり複雑となります.元々別のプロジェクトですから,これは当然です.したがって,子ウィンドウの管理やメニュー等の操作を実装するには,ある意味危険な操作かも知れませんが,EXE と DLL の双方でクラスのインスタンスを参照しなければ無理があると思われます.
実際,このサンプルコードでは TApplication と TScreen を DLL の関数の引数に使用しています.このような実装をした場合,その DLL はその開発言語専用となり,他の開発言語からは利用できない DLL となります.
個人的な感想ですが,DLL 内で MDI の子フォームを生成して使用するというのは,DLL 本来の特徴を利用した仕様とは言えない気がします.VCL にとっても「想定外」なのではないでしょうか.
もっとも,筆者の場合,DLL 内でフォームを生成して利用するということ自体にも,その必要性について疑問をもっています.
もちろん,アプリケーションの実装上の都合から,そのアプリケーション専用の DLL を作成するという状況は考えられますが,DLL での処理は DLL 内での自己完結型が基本と思われます.そうでなければ DLL 化する利点と言うか,意味がほとんど失われてしまいます. |
|
図15
実行画面 |
リスト4
DLL のフォームを MDI の子フォームにするための DLL の基本的なコード例
フォームの FormStyle の値をオブジェクト・インスペクタで fsMDIChild にしておく |
library DllTest_DLL;
{$R *.dres}
uses
Winapi.Windows,
System.Classes,
Vcl.Themes,
VCL.Styles,
Vcl.Controls,
Vcl.Forms,
DllFormUnit in 'DllFormUnit.pas' {DllForm};
{$R *.res}
var
SaveDllProc : procedure(Reason: Integer);
SaveApplication : TApplication;
SaveScreen : TScreen;
//-----------------------------------------------------------------------------
// DllProc関数の処理
// 本サンプルでは,DllEntryという名前に置き換えている
//-----------------------------------------------------------------------------
procedure DLLEntry(Reason: Integer);
var
VclH : TStyleManager.TStyleServicesHandle;
begin
if Reason = DLL_PROCESS_ATTACH then begin
//元のTApplicationとTScreen変数を退避
SaveApplication := Application;
SaveScreen := Screen;
//DLLのプロセス生成時の処理
//リソースからVCLスタイルの情報を取得してセット
if FindResource(HInstance, 'Resource_2', 'VCLSTYLE') <> 0 then begin
if TStyleManager.TryLoadFromResource(HInstance,
'Resource_2',
'VCLSTYLE',
VclH) then begin
if VclH <> nil then TStyleManager.SetStyle(VclH);
end;
end;
end else
if Reason = DLL_PROCESS_DETACH then begin
//退避したTApplicationとTScreen変数を元に戻す
Application := SaveApplication;
Screen := SaveScreen;
//その他のDLLプロセス終了時の処理
end;
//元のDllProcを実行
if Assigned(SaveDllProc) then begin
if Reason >= 0 then begin
SaveDllProc(Reason);
end;
end;
end;
//-----------------------------------------------------------------------------
// 外部から呼び出して実行する関数
// DllFormというフォームを表示
//-----------------------------------------------------------------------------
function ShowTestForm(App: TApplication; AScreen: TScreen): HWND; stdcall;
begin
Application := App;
Screen := AScreen;
DllForm := TDllForm.Create(Application);
DllForm.Show;
Result := DllForm.Handle;
end;
//-----------------------------------------------------------------------------
exports
ShowTestForm;
//-----------------------------------------------------------------------------
// DLL初期化時の処理
// DllProcはDLLのプロセス,スレッドの開始.終了時に自動実行される(SysInit.pas)
//-----------------------------------------------------------------------------
begin
//元のDllProcを退避
SaveDllProc := @DllProc;
DllProc := @DLLEntry;
//DLL初期化時に1回だけ実行
DLLEntry(DLL_PROCESS_ATTACH);
end.
リスト5
DLL を使用する側のテスト用プログラムのコード
フォームの FormStyle の値をオブジェクト・インスペクタで fsMDIForm にしておく |
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private 宣言 }
public
{ Public 宣言 }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function ShowTestForm(App: TApplication; AScreen: TScreen): HWND;
stdcall; external 'DllTest_DLL.dll';
//-----------------------------------------------------------------------------
// DLL内のShowTestForm関数を実行する
//-----------------------------------------------------------------------------
procedure TForm1.Button1Click(Sender: TObject);
begin
ShowTestForm(Application, Screen);
end;
end.
|