Delphi Programming / Object Pascal
[掲載 2018年11月04日] [更新 2018年11月19日] Delphi サンプルプログラム集
826_クリップボードのデータの退避と復元
動作確認等
Windows 7 U64(SP1) + Delphi XE5(UP2) Pro VCL-32
826_Clipboard_SaveLoad.zip [3,535 KB] 2018年11月19日版 (EXE 同梱)
画像,テキストあるいはファイルをクリップボードから取得したり,クリップボードにセットするサンプルが以下の記事にあります.それらのサンプルでは,クリップボードのフォーマット形式の 1 つが操作対象となっています.
本ページのサンプルは,現在クリップボードにセットされているデータを全てメモリストリームに保存します.その保存したクリップボードのデータをクリップボードにセットすることができます.
本サンプルでは,貼り付け操作可否のチェックの記事のサンプルで使用している TNkClipDetector コンポーネントを使用しています.
[備考]
Windows 10 では 2018年5月のアップデートで,制限はありますが,クリップボードの履歴の利用と他の端末との共有ができるようになりました.
その一方で,クリップボードを狙った新規のマルウェアも登場しているようです.
01_ クリップボードのデータをメモリストリームに保存
以下が実際のサンプルプログラムのコードです.
[退避] ボタンでクリップボードの内容をメモリストリームに格納します.クリップボードにデータがある時だけ操作できます.[復元] ボタンでメモリストリームのデータをクリップボードに貼り付け,つまりセットします.[クリア] はクリップボードを空にします.
サイズが大きいフォルダやファイルを退避したりクリップボードにセットすると時間がかかります.時間がかかる処理は,別のスレッドで処理するのが一般的です.
図1
設計時の画面
図2
実行時の画面
リスト1
クリップボードのデータをメモリストリームに保存.保存したデータをクリップボートにセット
copy code
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, NkClipDetector;
type
TForm1 = class (TForm)
Button1: TButton;
Button2: TButton;
Button3: TButton;
NkClipDetector1: TNkClipDetector;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Button3Click(Sender: TObject);
procedure NkClipDetector1ClipChange(Sender: TObject);
private
{ Private 宣言 }
public
{ Public 宣言 }
FMemStream : TMemoryStream;
end ;
procedure ClipBoardSaveToStream(AStream: TMemoryStream);
procedure ClipBoardLoadFromStream(AStream: TMemoryStream);
function WERDigThrough(const ExpInfo: TExceptionPointers): Integer; stdcall ;
function EOSErrorContinue(ExceptionInfo : TExceptionPointers): Integer; stdcall ;
function AddVectoredExceptionHandler(FirstHander: ULONG; VectoredHandler: Pointer):
PVOID; stdcall ; external 'kernel32.dll' ;
var
Form1: TForm1;
implementation
uses
Clipbrd;
{$R *.dfm}
// =============================================================================
// フォーム生成時の処理
// =============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
FMemStream := TMemoryStream.Create;
// エラー処理モードの設定
// 呼び出し側のアプリケーションで処理
SetErrorMode(SEM_FAILCRITICALERRORS);
// 例外発生時の処理用関数を設定(非デバッグ時専用)
SetUnhandledExceptionFilter(@WERDigThrough);
// 例外発生時の処理用関数を設定(デバッグ時専用)
AddVectoredExceptionHandler(0 , @EOSErrorContinue);
NkClipDetector1ClipChange(nil );
end ;
// =============================================================================
// フォーム破棄時の処理
// =============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
FreeAndNil(FMemStream);
end ;
// =============================================================================
// クリップボードのデータをメモリストリームに格納
// =============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
LInfoText : string ;
begin
Button1.Enabled := False;
Button2.Enabled := False;
Button3.Enabled := False;
if Clipboard.FormatCount = 0 then begin
LInfoText := 'クリップボードが空か無効です' ;
MessageBox(Handle, PChar(LInfoText), '情報' , MB_ICONINFORMATION);
end else begin
ClipboardSaveToStream(FMemStream); // 待避
if FMemStream.Size = 0 then begin
LInfoText := 'クリップボードのデータ取得に失敗しました' ;
MessageBox(Handle, PChar(LInfoText), '情報' , MB_ICONINFORMATION);
end ;
end ;
NkClipDetector1ClipChange(nil );
end ;
// =============================================================================
// 退避したクリップボードのデータ内容を復元
// =============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
Button1.Enabled := False;
Button2.Enabled := False;
Button3.Enabled := False;
ClipboardLoadFromStream(FMemStream);
NkClipDetector1ClipChange(nil );
end ;
// =============================================================================
// クリップボードの内容をクリア(消去)
// =============================================================================
procedure TForm1.Button3Click(Sender: TObject);
begin
Clipboard.Clear;
end ;
// =============================================================================
// TNkClipDetectorのClipChangeイベント
// クリップボードの内容に変更があると発生するイベント
// =============================================================================
procedure TForm1.NkClipDetector1ClipChange(Sender: TObject);
var
LFmtCnt : Integer;
begin
LFmtCnt := Clipboard.FormatCount;
Button1.Enabled := LFmtCnt > 0 ;
Button2.Enabled := FMemStream.Size > 0 ;
Button3.Enabled := LFmtCnt > 0 ;
end ;
// -----------------------------------------------------------------------------
// クリップボードのデータをメモリストリームに格納するルーチン
//
// CF_BITMAPまたはCF_DIBの時にGlobalLock,GlobalSizeで例外が発生することがある
// そこでWin32Checを実行してEOSError(OSのエラー)を発生させる
// するとRaiseLastOSErrorが実行されてtry - exceptで捕捉可能になる
// ただし,そのままではデバッガの例外通知のダイアログが表示されてしまう
// -----------------------------------------------------------------------------
procedure ClipBoardSaveToStream(AStream: TMemoryStream);
var
LhGlobal : HGLOBAL;
LFormatId : UINT;
LdwSize : DWORD;
LpBuffer : Pointer;
LIndex : Integer;
begin
AStream.Clear;
AStream.Position := 0 ;
Clipboard.Open;
try
for LIndex := 0 to ClipBoard.FormatCount - 1 do begin
LFormatId := ClipBoard.Formats[LIndex];
if not IsClipboardFormatAvailable(LFormatId) then Continue;
// クリップボードのデータを取得
LhGlobal := GetClipBoardData(LFormatId);
if LhGlobal > 0 then begin
try
try
// クリップボードのデータのポインタとデータサイズの取得
if Win32Check(GlobalLock(LhGlobal) <> nil ) then begin
LpBuffer := GlobalLock(LhGlobal);
end ;
if Win32Check(GlobalSize(LhGlobal) <> 0 ) then begin
LdwSize := GlobalSize(LhGlobal);
end ;
except
LdwSize := 0 ;
LpBuffer := nil ;
end ;
if (LdwSize > 0 ) then begin
// フォーマットの形式,データのサイズ,実査際のデータの順に格納
// 取り出す時もこの順番で取り出す
AStream.Write (LFormatID, SizeOf(LFormatId));
AStream.Write (LdwSize, SizeOf(LdwSize));
AStream.Write (LpBuffer^, LdwSize);
end ;
finally
GlobalUnlock(LhGlobal);
end ;
end ;
end ;
finally
Clipboard.Close;
end ;
end ;
// -----------------------------------------------------------------------------
// メモリストリームのデータをクリップボードにコピー(セット)するルーチン
// -----------------------------------------------------------------------------
procedure ClipBoardLoadFromStream(AStream: TMemoryStream);
var
LhGlobal : HGLOBAL;
LFormatId : UINT;
LdwSize : DWORD;
LpBuffer : Pointer;
begin
AStream.Position := 0 ;
Clipboard.Clear;
Clipboard.Open;
try
while True do begin
if AStream.Position >= AStream.Size then Break;
// クリップボードの形式(クリップボードの識別子)とデータサイズを取り出す
AStream.Read (LFormatId, SizeOf(LFormatId));
AStream.Read (LdwSize, SizeOf(LdwSize));
LhGlobal := GlobalAlloc(GMEM_MOVEABLE, LdwSize);
LpBuffer := GlobalLock(LhGlobal);
try
AStream.Read (LpBuffer^, LdwSize);
finally
GlobalUnlock(LhGlobal);
end ;
// クリップボードにデータをセット
SetClipboardData(LFormatId, LhGlobal);
end ;
finally
Clipboard.Close;
end ;
end ;
// -----------------------------------------------------------------------------
// SetUnhandledExceptionFilter関数のコールバック関数
// SetUnhandledExceptionFilter関数は非デバッグ時専用の関数
//
// EXE実行時に例外が発生するとWER(Windows Error Report)のダイアログが現れる
// するとアプリが終了またはハングアップする
// WERを表示しないでアプリも終了させないようにするための処理
// -----------------------------------------------------------------------------
function WERDigThrough(const ExpInfo: TExceptionPointers): Integer; stdcall ;
const
EXCEPTION_EXECUTE_HANDLER = 1 ;
EXCEPTION_CONTINUE_SEARCH = 0 ;
begin
Result := EXCEPTION_EXECUTE_HANDLER;
end ;
// -----------------------------------------------------------------------------
// AddVectoredExceptionHandler関数のコールバック関数(デバッグ時専用)
//
// 本サンプルではOSの例外(DLL内部での例外))を捕捉するようにしている
// そのままではデバッガの例外通知のダイアログが表示される
// そのダイアログを表示しないようにするための処理
// 時々機能しないことがある
//
// Delpi XE5にはAddVectoredExceptionHandler関数は実装されていない
// -----------------------------------------------------------------------------
function EOSErrorContinue(ExceptionInfo : TExceptionPointers): Integer; stdcall ;
const
EXCEPTION_EXECUTE_HANDLER = 1 ;
EXCEPTION_CONTINUE_SEARCH = 0 ;
begin
Result := EXCEPTION_CONTINUE_SEARCH;
end ;
end .
02_ Win32Check を使用して OS 例外を try - except で捕捉
上のサンプルでは,クリップボードのデータを取得する際,データのポインタとデータサイズ取得のコードが以下のようになっています.Win32Check で OS の例外発生をチェックし,その例外を捕捉するための try - except を追加しています.
リスト2
上のサンプルにおけるクリップボードのデータのポインタとデータサイズ取得のコード
copy code
try
// クリップボードのデータのポインタとデータサイズの取得
if Win32Check(GlobalLock(LhGlobal) <> nil ) then begin
LpBuffer := GlobalLock(LhGlobal);
end ;
if Win32Check(GlobalSize(LhGlobal) <> 0 ) then begin
LdwSize := GlobalSize(LhGlobal);
end ;
except
LdwSize := 0 ;
LpBuffer := nil ;
end ;
もし Windows API の関数 GlobalLock あるいは GlobalSize で例外が発生しなければ,上のコードは以下のコードで十分です.
リスト3
クリップボードのデータのポインタとデータサイズ取得の本来のコード
copy code
LpBuffer := GlobalLock(LhGlobal);
LdwSize := GlobalSize(LhGlobal);
しかし,上のクリップボードのデータ取得のサンプルではこの部分で例外が発生し,下図のようにデバッガ例外通知のダイアログ現れることがあります.
この例外は,クリップボードのフォーマット形式に CF_BITMAP と CF_DIB の両方が含まれている時に発生することがあります.発生しないこともあります.どちらか一方だけを検出してデータを取得するようなコーディングでは発生しません.
この例外が発生しても,デバッガ例外通知のダイアログで [継続] を選択すればクリップボードのデータは取得できます.
クリップボードに CF_BITMAP 形式でデータをセットすると CF_DIB のデータもセットされます.逆にフォーマット形式が CF_DIB のデータをセットすると CF_BITMAP のデータもセットされます.おそらくこのことが関係しているのかも知れません.
図3
OS 例外
デバッガ例外通知のダイアログで [継続] をクリックすればデータは取得できると書きましたが,実はそのままでは正しいデータが取得できる保証はありません.
GlobalLock 関数と GlobalSize 関数は Windows API の関数です.つまり Windows が提供している DLL 内部で発生する例外です.このような OS の例外は try - except では捕捉できません.
本サンプルでは,Win32Check
関数を使用して OS の例外を try - except で捕捉できるようにしています.それだけではデバッガ例外通知のダイアログは表示されてしまいます.
そこで SetErrorMode
関数でエラーの処理モードを設定します.更に,AddVectoredExceptionHandler 関数のコールバック関数内で,デバッガ例外通知のダイアログで [継続] を選択した時と同じ操作をするように設定しています.
残念ながら,これらの処理をしてもデバッガ例外通知のダイアログが現れることがあります.しかし [継続] をクリックすれば try - except は確実に実行されますから,間違いなくクリップボードのデータは取得できます.
copy code
SetErrorMode(SEM_FAILCRITICALERRORS);
リスト5
AddVectoredExceptionHandler 関数のコールバック関数.デバッグ時の OS 例外処理用
copy code
function EOSErrorContinue(ExceptionInfo : TExceptionPointers): Integer; stdcall ;
const
EXCEPTION_EXECUTE_HANDLER = 1 ;
EXCEPTION_CONTINUE_SEARCH = 0 ;
begin
Result := EXCEPTION_CONTINUE_SEARCH;
end ;
[備考]
ネイティブ OS の例外の一覧が Delphi の IDE で確認できます.
メニューの [ツール] [オプション]
のダイアログで,一番下にある [ネイティブ OS 例外] を選択すると表示されます.ここでの設定は,開発するアプリ全てが対象となります.
03_ Windows Error Report ( WER ) によるプログラムの終了を回避
前項で述べた処理をしても,OS の例外が発生した場合,作成した EXE を直接実行 (デバッガを使わずに実行 [Shift] + [Ctrl] + [F9]) すると下図のようなダイアログが現れてプログラムが強制終了されてしまいます.
本サンプルではこれを回避するために,SetUnhandledExceptionFilter 関数を使用して,try - except に処理を戻すようにしています.したがって,本サンプルでは EXE を直接実行する場合,このダイアログが現れることはありません.
SetUnhandledExceptionFilter 関数は非デバッグ時にだけ有効な関数です.デバッグ時には処理されません.一方,AddVectoredExceptionHandler 関数は,デバッグ時にだけ有効な関数です.
図4
EXE を直接実行した場合
[キャンセル] をクリックするとプログラムが終了する
例外が発生しない場合はこのダイアログは現れない
図5
上のダイアログで [キャンセル] しなかった場合
WER (Windows Error Report) のダイアログが現れる
プログラムは強制終了となる
リスト6
SetUnhandledExceptionFilter 関数のコールバック関数.非デバッグ時の OS 例外処理用
copy code
function WERDigThrough(const ExpInfo: TExceptionPointers): Integer; stdcall ;
const
EXCEPTION_EXECUTE_HANDLER = 1 ;
EXCEPTION_CONTINUE_SEARCH = 0 ;
begin
Result := EXCEPTION_EXECUTE_HANDLER;
end ;
04_ 保存したメモリストリームからビットマップ画像を取得
例えば画像をコピーして本サンプルでメモリストリームに保存したとします.このメモリストリームからビットマップを取得するにはビットマップヘッダ部の処理が必要です.ヘッダ部の具体的な処理については以下の記事のコードを参考にしてください.
もう 1 つの方法はクリップボードを利用する方法です.該当するメモリストリームのデータをクリップボードにコピーして,そのクリップボードのデータを取得します.以下はその例です.
図6
実行画面
リスト7
保存したメモリストリームからビットマップ画像を取得.クリップボードを利用する例
copy code
procedure ClipBoardLoadFromStream(AStream: TMemoryStream);
var
LhGlobal : HGLOBAL;
LFormatId : UINT;
LdwSize : DWORD;
LpBuffer : Pointer;
LBitmap : TBitmap;
LSaveFlag : Boolean;
begin
AStream.Position := 0 ;
LSaveFlag := False;
while True do begin
if AStream.Position >= AStream.Size then Break;
// クリップボードの形式(クリップボードの識別子)とデータサイズを取り出す
AStream.Read (LFormatId, SizeOf(LFormatId));
AStream.Read (LdwSize, SizeOf(LdwSize));
LhGlobal := GlobalAlloc(GMEM_MOVEABLE, LdwSize);
LpBuffer := GlobalLock(LhGlobal);
try
// 実際には読み出さなくてもよい
// メモリストリームの読み出し位置を変更すればよい
AStream.Read (LpBuffer^, LdwSize);
finally
GlobalUnlock(LhGlobal);
end ;
if (LFormatId = CF_BITMAP) or (LFormatId = CF_DIB) then begin
// クリップボードにデータをセット
Clipboard.Clear;
Clipboard.Open;
SetClipboardData(LFormatId, LhGlobal);
Clipboard.Close;
Clipboard.Open;
LBitmap := TBitmap.Create;
try
// クリップボードからビットマップを取得
// 以下でもOK
// LBitmap.Handle := Clipboard.GetAsHandle(CF_BITMAP);
LBitmap.Assign(Clipboard);
// 保存
LBitmap.SaveToFile('000.bmp' );
finally
FreeAndNil(LBitmap);
Clipboard.Close;
end ;
Clipboard.Close;
LSaveFlag := True;
Break;
end ;
end ;
if not LSaveFlag then begin
MessageBox(0 , 'ビットマップデータはありません' , '情報' , MB_ICONWARNING);
end ;
end ;
05_ 保存したメモリストリームからテキストデータを取得
保存したメモリストリームからテキストを取得するサンプルです.
テキストデータはメモリストリームのデータそのものですから,そのまま TStringStream 等で取得できます.
図7
実行画面
リスト8
保存したメモリストリームからテキストデータを取得
copy code
procedure ClipBoardLoadFromStream(AStream: TMemoryStream);
var
LFormatId : UINT;
LdwSize : DWORD;
LStream : TStringStream;
LSaveFlag : Boolean;
begin
AStream.Position := 0 ;
LSaveFlag := False;
LStream := TStringStream.Create;
try
while True do begin
if AStream.Position >= AStream.Size then Break;
// クリップボードの形式(クリップボードの識別子)とデータサイズを取り出す
AStream.Read (LFormatId, SizeOf(LFormatId));
AStream.Read (LdwSize, SizeOf(LdwSize));
// メモリストリームからデータを読み出す
LStream.Clear;
LStream.Size := LdwSize;
AStream.Read (LStream.Bytes[0 ], LdwSize);
if (LFormatId = CF_TEXT) then begin
// 保存
LStream.SaveToFile('000.txt' );
LSaveFlag := True;
Break;
end ;
end ;
finally
FreeAndNil(LStream);
end ;
if not LSaveFlag then begin
MessageBox(0 , 'テキストデータはありません' , '情報' , MB_ICONWARNING);
end ;
end ;
上のコードは CF_TEXT 形式のテキストデータを取得する例です.
保存したメモリストリームから CF_UNICODETEXT 形式のテキストデータを取得するには,例えば以下のように TStringList を使用して取得できます.TStringList は文字列を読み込む時も保存する時もコードページの指定,つまりエンコードの指定が可能です.このことを利用します.
リスト9
保存したメモリストリームから CF_UNICODETEXT 形式のテキストデータを取得
copy code
if (LFormatId = CF_UNICODETEXT) then begin
LStrList := TStringList.Create;
try
LStrList.LoadFromStream(LStream, TEncoding.Unicode);
LStrList.SaveToFile('000_Unicode.txt' , TEncoding.Unicode);
Form1.Memo1.Lines.Text := LStrList.Text;
finally
FreeAndNil(LStrList);
end ;
LSaveFlag := True;
Break;
end ;
end ;
また,メモリストリームのバイトデータをそのまま String 型の変数のデータとして以下のようにしても取得できます.この String 型の変数の文字列をテキスト表示用のコントロールに表示します.以下のコードでは TMemo に表示します.Delphi 2009 以降の TMemo はユニコードに対応したコントロールですからそのまま表示できます.
TMemo のテキストを保存する時はエンコードを指定します.エンコードを指定しないと,ユニコードな文字が文字化けします.メモリストリームのバイトデータを格納する文字列の型は,UnicodeString 型でも構いません.
リスト10
保存したメモリストリームから CF_UNICODETEXT 形式のテキストデータを取得
copy code
if (LFormatId = CF_UNICODETEXT) then begin
SetLength(LText, LStream.Size * SizeOf(Char));
LStream.ReadBuffer(LText[1 ], LStream.Size);
Form1.Memo1.Lines.Text := LText;
Form1.Memo1.Lines.SaveToFile('000_Unicode.txt' , TEncoding.Unicode);
LSaveFlag := True;
Break;
end ;
end ;
06_ 保存したメモリストリームからファイル名のリストを取得
エクスプローラでコピーしたファイルのリストをメモリストリームに保存した後,そのメモリストリームからファイル名のリストを取得するサンプルです.
メモリストリームの先頭はヘッダ部です.このヘッダ部は TDropFiles レコード型 (_DROPFILES 構造体) のデータです.実際のファイル名はこのヘッダ部の後に記録されています.
ファイル名は Null 終端文字列が複数結合された形で格納されています.本サンプルではこれを先頭から順番に取り出して TStringList に格納していきます.
図8
エクスプローラでコピーしたファイルのリストの例
リスト11
保存したメモリストリームからエクスプローラでコピーしたファイル名のリストを取得
copy code
procedure ClipBoardLoadFromStream(AStream: TMemoryStream);
var
LFormatId : UINT;
LdwSize : DWORD;
LStream : TBytesStream;
LGetFlag : Boolean;
LDropFiles : TDropFiles;
LDataSize : Integer;
LSrcPChar : PChar;
LSrcAnsiPChar : PAnsiChar;
LPChar : PChar;
LAnsiPChar : PAnsiChar;
LStrList : TStringList;
begin
AStream.Position := 0 ;
LGetFlag := False;
LStream := TBytesStream.Create;
try
while True do begin
if AStream.Position >= AStream.Size then Break;
// クリップボードの形式(クリップボードの識別子)とデータサイズを取り出す
AStream.Read (LFormatId, SizeOf(LFormatId));
AStream.Read (LdwSize, SizeOf(LdwSize));
// メモリストリームからデータを読み出す
LStream.Clear;
LStream.Size := LdwSize;
AStream.Read (LStream.Bytes[0 ], LdwSize);
if (LFormatId <> CF_HDROP) then Continue;
// ヘッダ部の情報を取り出す
LStream.Position := 0 ;
LStream.Read (LDropFiles, SizeOf(LDropFiles));
if LDropFiles.fNC then Break;
LDataSize := LStream.Size - LDropFiles.pFiles;
LStrList := TStringList.Create;
try
if LDropFiles.fWide then begin
// ユニコード (Unicode) の場合
// 文字列を格納するメモリ領域を確保
LSrcPChar := AllocMem(LDataSize);
try
// ファイル名のデータ部分をPChar型として取得
LStream.Position := LDropFiles.pFiles;
LStream.ReadBuffer(LSrcPChar^, LDataSize);
LPChar := LSrcPChar;
// 各々のNull終端文字列を取得する
while (StrLen(LPChar) > 0 ) do begin
// 取得した文字列をリストに格納
LStrList.Add(LPChar);
// 取得元のポインタを移動(+1はnull終端(#0)の分)
// LPChar := LPChar + Length(LPChar) + 1; でもよい
LPChar := LPChar + StrLen(LPChar) + 1 ;
end ;
finally
FreeMem(LSrcPChar);
end ;
end else begin
// 非ユニコード (Ansi) の場合
// 手順はユニコードの場合と同じ
LSrcAnsiPChar := AllocMem(LDataSize);
try
LStream.Position := LDropFiles.pFiles;
LStream.ReadBuffer(LSrcAnsiPChar^, LDataSize);
LAnsiPChar := LSrcAnsiPChar;
while (Length(LAnsiPChar) > 0 ) do begin
LStrList.Add(LAnsiPChar);
LAnsiPChar := LAnsiPChar + Length(LAnsiPChar) + 1 ;
end ;
finally
FreeMem(LSrcAnsiPChar);
end ;
end ;
// 取得結果を表示
LStrList.Sort;
Form1.Memo1.Lines.Text := LStrList.Text;
LGetFlag := LStrList.Count > 0 ;
finally
FreeAndNil(LStrList);
end ;
Break;
end ;
finally
FreeAndNil(LStream);
end ;
if not LGetFlag then begin
Form1.Memo1.Lines.Clear;
MessageBox(0 , 'ファイルリスト関係のデータはありません' , '情報' , MB_ICONWARNING);
end ;
end ;