公告

國明的網路筆記

2013年4月11日 星期四

一些 BCB6 應用原始程式


BCB6 源碼任務心得分享
取材自【源代碼工作室】:http://home.kimo.com.tw/bruce0829/

 
【取得 TMemo 物件的游標位置】
■ 取得編輯游標(Edit Cursor)的目前座標

求 y 及 x 座標

int y = Memo1->CaretPos.y;
int x = Memo1->CaretPos.x;

int y = SendMessage(Memo1->Handle,EM_LINEFROMCHAR,Memo1->SelStart,0);
int x = Memo1->SelStart - SendMessage(Memo1->Handle,EM_LINEINDEX,y,0);

int y = Memo1->Perform(EM_LINEFROMCHAR, -1, 0);
int x = Memo1->SelStart - Memo1->Perform(EM_LINEINDEX, -1, 0);

根據測試 (BCB 5.0) 使用 Memo1->CaretPos.y , Memo1->CaretPos.x
會有 bug , 在某些位置會讀出負值出來
故下面的例子不使用

//-------------------------------------------------------------------------
//顯示目前編輯座標的函式,本函式可放於 Memo 的 OnClick() 及 OnKeyUp() 事件中
//隨時將目前編輯座標位置顯示在 Label1 中
//-------------------------------------------------------------------------
void __fastcall TForm1::DispPosition()
{
int begin_position=1; //座標開始位置(筆者喜歡把第一列看做是 Line 1 而不是 Line 0)
int y=SendMessage(Memo1->Handle,EM_LINEFROMCHAR,Memo1->SelStart,0);
int x=Memo1->SelStart - SendMessage(Memo1->Handle,EM_LINEINDEX,y,0);

Label1->Caption="Line:"+IntToStr(y+begin_position)+" Col:"+IntToStr(x);
}


【 將 MEMO 某列反白】

直接在程式碼中做說明 , 使用方法為
若要將 MEMO1 第 5 列整列反白則呼叫
LineIndexTo(5);

void __fastcall TForm1::LineIndexTo(int row)
{
long TopRow,StartPostion,EndPostion;

//將 Memo 的 View 移到目的行
TopRow=SendMessage(Memo1->Handle, EM_GETFIRSTVISIBLELINE, 0, 0); //抓取目前 Memo1 中可視的第一列(Row)列號
SendMessage(Memo1->Handle, EM_LINESCROLL, 0, row - TopRow); //向下捲 row - TopRow 列 (若為負值則向上)

//取得 row 頭尾字元位置
StartPostion = SendMessage(Memo1->Handle, EM_LINEINDEX, row, 0); //取得 Memo 中第 row 列開頭的字元位置
EndPostion = SendMessage(Memo1->Handle, EM_LINEINDEX, (row + 1), 0) - 1 ;

//將所指定的範圍位置反白
SendMessage(Memo1->Handle, EM_SETSEL, StartPostion, EndPostion);
//上列程式碼同下
//Memo1->SelStart=StartPostion;
//Memo1->SelLength=EndPostion-StartPostion-1;

Memo1->SetFocus();
}


【 如果您不想寫一個資料庫程式還要手動去設 BDE 別名時】
void __fastcall TForm1::FormCreate(TObject *Sender)
{
shortString MyAlias="MyAlias";
if (!Session->IsAlias(MyAlias))
{
Session->AddStandardAlias(MyAlias, ExtractFilePath(Application->ExeName)+"DB\\LocalDB", "PARADOX");
//Session->SaveConfigFile(); //不存也可,但程式結束,此臨時別名也會消失
ShowMessage("自動加入資料庫別名:"+MyAlias);
}

 
【資料庫的搜尋方法】
常用的搜尋方法有兩種,一種是 FindKey方法,另一種是 Locate方法;FindKey 的方法在早期就已使用,
現在只是為了相容性而存在,而且要使用FindKey方法來搜尋某鍵值,必須存在該鍵值的索引;
而 Locate方法不一定需要索引(有則會自動引用),建議讀者盡量使用Locate方法,兩者方法都會在
搜尋到鍵值時會將資料庫記錄指標停在找到的記錄上,並傳回一個 true 值

■ FindKey

使用 FindKey只搜尋"一個"鍵位值時 (要先設好 Table1 之索引鍵)
Table1->FindKey(new TVarRec(搜尋值), 0);

使用 FindKey搜尋"複合鍵位"(兩個以上)值時 (要先設好 Table1 之索引鍵)
Table1->FindKey(OPENARRAY(TvarRec, (搜尋值1, 搜尋值2, 搜尋值3)))

■ Locate

使用 Locate 只搜尋"一個鍵位"值時 (Table1 不需有索引)
Table1->Locate(“欲搜尋之欄位名稱”, 搜尋值, TLocateOptions() << loPartialKey);

使用 Locate 搜尋"複合鍵位"(兩個以上)值時 (Table1 不需有索引)
Variant locvalues[]={搜尋值1, 搜尋值2, 搜尋值3};
Table1->Locate("欄位名稱1; 欄位名稱2; 欄位名稱3", VarArrayOf(locvalues,(sizeof(locvalues)/16-1)),TLocateOptions() << loPartialKey);

註 : 有些市售騙錢 BCB 書籍講了一堆...卻只單純介紹"單鍵"搜尋 ....

 
【防止程式重複執行】
■ 要防止自己的程式重複執行,使用的方法有好幾種,有些方法甚至要去修改*.bpr檔(C++ Builder 專案檔),
但筆者不建議讀者任意修改 C++ Builder 自動維護的專案檔;使用本節介紹的方法只要將下列程式碼原封不動的
抄到您的 FormCreate() 事件中即可,不但不會讓相同的程式同時存在第二個實體,而且在執行時發現第一個實體
已存在而且是最小化時,還會將其彈出展開於桌面上....


//-------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
Application->Title=Form1->Caption;
  //TrayIcon1->Hint=Form1->Caption;
HANDLE PrevInstHandle;
HANDLE Mutex = OpenMutex(SYNCHRONIZE,false,Application->Title.c_str());

if (Mutex != NULL)
{
String AppTitle=Application->Title;
SetWindowText(Application->Handle,NULL);
PrevInstHandle = FindWindow("TApplication", AppTitle.c_str());
if (PrevInstHandle != 0)
{
if (IsIconic(PrevInstHandle))
ShowWindow(PrevInstHandle, SW_RESTORE);
else
BringWindowToTop(PrevInstHandle);

SetForegroundWindow(PrevInstHandle);
}
Application->ShowMainForm = false;
Application->Terminate();
}
else CreateMutex(null, false, Application->Title.c_str());

// 接下來可以插入您其它要寫在 FormCreate 中的程式

}

■ 方法二

 // Please ADD Your Form1 header file
//Form1.hpp
HANDLE mx;

//Please ADD Form1::Create 1st line
Form1::Create(..)
{
mx = CreateMutex( NULL, true, "AbacusPosAP" );
if( GetLastError() ){
Application->Terminate(); //Finsh Application
}
}


//Please ADD Form1::Close Final Line
Form1::Close(..)
{
.................
......
ReleaseMutex( mx );
}

 
【如何讓程式執行時,馬上隱藏於桌面右下角】
1.在設計程式時,加入一個 TrayIcon 元件(在 C++Builder 之 Sample 元件頁中第二個)於主 Form 中,
並將該元件 Visible 屬性設為 true

2.在主 Form 的 OnActive 事件中寫入兩行程式碼:
Application->Minimize();
ShowWindow(Application->Handle, SW_HIDE);

3.以後這個程式於執行時,會馬上隱藏於桌面右下角成為 Tray Icon

重點在第二點 ....

 
源碼任務心得分享 : 以程式控制 Windows 2000 關機
以程式控制 Windows 2000 關機

//-------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
HANDLE hToken;
TOKEN_PRIVILEGES tkp;
OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY,
&hToken);

LookupPrivilegeValue(null, SE_SHUTDOWN_NAME,
&tkp.Privileges[0].Luid);
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken,
false,
&tkp,
0,
(PTOKEN_PRIVILEGES)null,
0);

// 下面八個動作中任選一種執行 ....

// 一般關閉,關閉前會詢問其它執行中的程式是否存檔
//ExitWindowsEx(EWX_LOGOFF,0); // 登出, 重新登入使用者
//ExitWindowsEx(EWX_REBOOT,0); // 重新開機
//ExitWindowsEx(EWX_SHUTDOWN,0); // 結束作業系統,出現"您可以放心關機畫面"
//ExitWindowsEx(EWX_POWEROFF,0); // 結束作業系統,並關閉電源(僅支援ATX 規格)

// 強制關閉,其它執行中的應用程式資料並不會被儲存
//ExitWindowsEx(EWX_LOGOFF | EWX_FORCE,0); // 登出, 重新登入使用者
//ExitWindowsEx(EWX_REBOOT | EWX_FORCE,0); // 重新開機
//ExitWindowsEx(EWX_SHUTDOWN | EWX_FORCE,0); // 結束作業系統
//ExitWindowsEx(EWX_POWEROFF | EWX_FORCE,0); // 結束作業系統,並關閉電源 (僅支援ATX 規格)
}

 
【 結束或最小化指定的程式】
■ 結束指定的程式

要結束指定的程式,得先要知道要結束的對象程式標題名稱是什麼,然後利用FindWindow() 來找出其視窗 HANDLE,
再呼叫 PostMessage() 送出 WM_CLOSE 訊息以結束該視窗,PostMessage()的用法與 SendMessage()完全相同,
所不同的是 SendMessage() 會等待接收訊息的視窗處理完訊息後才返回,而PostMessage()則是送出訊息命令後馬上返回

//-------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
HANDLE DestHandle;
DestHandle = FindWindow(null,"test.txt - 記事本");
if (DestHandle != 0) PostMessage(DestHandle, WM_CLOSE, 0, 0);
}
//-------------------------------------------------------------------

■ 最小化指定的程式

指定某一程式最小化有三種做法:(DestHandle 承上節求出)
1. CloseWindow(DestHandle); (勿以為這是「結束」指定程式的用法)
2. ShowWindow(DestHandle, SW_MINIMIZE);
3. SendMessage(DestHandle, WM_SIZE, SIZE_MINIMIZED, 0);

■ FindWindow() 的問題

以上,當我們在尋找目標視窗時,FindWindow()內需一字不漏的填入完整的視窗標題名稱,否則會找不到目標視窗;
所以我們進一步利用 GetWindow()函式,搜尋全部可見視窗,再逐一比對其視窗標題名稱,這樣就可做到所謂「概略搜尋」,
以下的程式範例會將桌面上所有視窗標題名稱含有”記事本”字樣的視窗通通關閉

//-------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
HANDLE hCurrentWindow = GetWindow(Handle,GW_HWNDFIRST);
char buff[255];
String Text;

while (hCurrentWindow!=0)
{
if ( ( GetWindowText(hCurrentWindow,buff,255)>0) &&
IsWindowVisible(hCurrentWindow) )
{
//ShowMessage(buff);
Text=buff;
if (Text.Pos("記事本")!=0)
{
PostMessage(hCurrentWindow, WM_CLOSE, 0, 0);
}
}
hCurrentWindow = GetWindow(hCurrentWindow,GW_HWNDNEXT);
}
}

 
『執行特定程式或文件 ShellExecute』
使用 ShellExecute

一般我們看到市售程式中,按個按鈕可連到開發公司的首頁,或按個按鈕可自動開啟 OutLook Express 準備寄信給作者,這些功能都可用 ShellExecute 實做之

■ 語法

ShellExecute(
HWND hwnd, // 父視窗 Handle
LPCTSTR lpOperation, // 開啟或操作方式
LPCTSTR lpFile, // 檔案名稱
LPCTSTR lpParameters, // 參數內容
LPCTSTR lpDirectory, // 命令所在目錄
int nShowCmd // 執行時視窗型態
);

不重要的參數可用 null 取代,其它詳細內容請參考線上說明

■ 應用

用「系統預設」的瀏覽器開啟某一個網址:
ShellExecute(null,null,"http://www.hinet.net/",null,null,SW_SHOW);

用「系統預設」郵件軟體開啟一個新郵件:
ShellExecute(null,null,"mailto:test@ms5.hinet.net",null,null,SW_SHOW);

ShellExecute(null,null,"mailto:test@ms5.hinet.net?Subject=哈囉&body=您好",null,null,SW_SHOW);

用「系統預設」的程式開啟某個檔案 (不用管該檔案是 Word 檔還是 Excel 檔,系統會先自動呼叫開啟該檔案型態的主程式):
ShellExecute(null,null,"c:\\test.doc",null,null,SW_SHOW);

ShellExecute(null,null,"c:\\test.xls",null,null,SW_SHOW);

指定用某程式開啟或列印某個檔案:
ShellExecute(null,"open","notepad.exe","c:\\test.txt", null,SW_SHOW);

ShellExecute(null,"print","notepad.exe","c:\\test.txt", null,SW_SHOW);

啟動某個應用程式:
ShellExecute(null,null,"c:\\PosAgent.exe",null,null,SW_SHOW);
或啟動某個內部命令
ShellExecute(null,null,"cmd.exe","/k dir c:\\",null, SW_SHOW);

 
『如何模擬 FileListBox 功能』
如果程式需要抓取一堆文字檔來處理,可使用現成的 FileListBox 元件並將元件之Directory屬性指向目標目錄位址,我們要找的文字檔就會出現在 FileListBox 中了,利用修改其 ItemIndex 屬性值,就可逐筆取出 FileListBox1->FileName 傳回之檔案名稱;FileListBox 元件還有個好處,那就是它還可利用 Mask 屬性過濾副檔名,我們可以要求只列出某一目錄中副檔名為 *.TXT (可同時選擇多種不同副檔名)的檔案;但如果程式是一個無 Form 的不可視程式,或程式的 Form 中沒有地方放這個 FileListBox 元件,
而我們卻需要其「列出檔案清單」的功能,在背景做一些檔案處理的動作(如一直偵測某一目錄發現收到文字檔後 , 做轉資料庫的動作)
但畫面上卻不需要上插入 FileListBox 這個元件,那該如何做呢?

1.以程式碼建立一個 TstringList 物件(非可視物件)
2.利用 FindFirst() 函式收集檔案名稱,放入這個 TstringList 物件中
3.TstringList 物件的使用方法類似 ListBox 或 FileListBox 元件
4.用完要自行釋放這個 TstringList 物件

void __fastcall TForm1::Button1Click(TObject *Sender)
{
String Dir;
String FileName;
TSearchRec SearchRec;
int iAttributes = 0;
iAttributes |= faAnyFile * true;

//偵測 c:\ 底下有無 *.tot 這個類型的檔案
//有的話則收集到 TempList 這個區域物件中 , 用完即釋放掉

Dir=("C:\\");

TStringList *TempList = new TStringList; // declare the list
try {
//use the string list
TempList->Clear();

//搜集 *.tot
if (FindFirst(Dir+"*.tot", iAttributes, SearchRec) == 0)
{
do {
if ((SearchRec.Attr & iAttributes) == SearchRec.Attr)
{
TempList->Add(SearchRec.Name);
}
} while (FindNext(SearchRec) == 0);

FindClose(SearchRec);
}


for ( int i = 0; i <= (TempList->Count-1); i++)
{
FileName=(Dir+TempList->Strings[i]);

//本迴圈可抓出所有符合條件之檔名至 FileName 變數中

//..........................
//可將後續運用程式碼寫在這裡
//..........................
}
}
__finally
{
delete TempList; // destroy the list object
ProgressBar2->Position=100;
Label2->Visible=true;
}

}

 
『利用迴圈變更50個TLabel property』
for (int i = 1; i <= 50; i++)
{
((TLabel *)FindComponent("Label"+IntToStr(i)))->Caption="";
}


 
源碼任務心得分享 : PACK 一個 Paradox Table
//---------------------------------------------------------------------------
// <<使用方法>> if (ParadoxPack("AbcPSV","TOT")) ShowMessage("資料庫 TOT ...PACK 完成");
// <<注意事項>>
// 1. 在呼叫 ParadoxPack 前 , 連到 TOT 的 TTable 元件 Active 必須是 false
// 2. 第一個傳入參數一定要用資料庫別名 , 不能用路徑
// (如 ParadoxPack("C:\\AbcPSV\\DB","TOT")) 是錯誤的 )
//---------------------------------------------------------------------------
bool _ParadoxPack(char *Alias, char *TblName)
{ hDBIDb hDb ;
DBIResult dbResult ;
CRTblDesc TblDesc ;
bool r=false;

//Initialize the BDE
DbiInit(null);

//Open a Database
DbiOpenDatabase(Alias,null,dbiREADONLY,dbiOPENSHARED,null,0,null,null,hDb);

memset((void *) &TblDesc, 0, sizeof(CRTblDesc));
lstrcpy(TblDesc.szTblName, TblName);
lstrcpy(TblDesc.szTblType, szPARADOX);
TblDesc.bPack = true;
dbResult = DbiDoRestructure(hDb, 1, &TblDesc, null, null,null, false);
if(dbResult == DBIERR_NONE) r=true;

//Close Database
DbiCloseDatabase(hDb);

return r;

}

 
『以程式碼建立 Paradox Table』
下面的程式碼會在您事先設好的 "My_Alias" BDE 別名路徑下
自動建立一個名叫 Plu.db 的資料表實體
(若名稱中加入日期流水號 , 每執行一次就會產生一個 Table)
欄位有字串及整數型態 , 並以 PLU_NO 欄位當 Primery Key

void __fastcall TForm1::Button1Click(TObject *Sender)
{

TTable *NewTable = new TTable(Form1);
NewTable->Active = false;
NewTable->TableType = ttParadox; //自行更改
NewTable->DatabaseName = "My_Alias"; //自行更改
NewTable->TableName = "Plu.db"; //自行更改

NewTable->FieldDefs->Clear();
NewTable->FieldDefs->Add("PLU_NO",ftString,16,true);
NewTable->FieldDefs->Add("TEN_CODE",ftString,4,false);
NewTable->FieldDefs->Add("DEPT_NO",ftString,8,true);
NewTable->FieldDefs->Add("PACK_AMT",ftInteger,0,true);
NewTable->FieldDefs->Add("PACK_QTY",ftInteger,0,true);
NewTable->FieldDefs->Add("UNIT_PRICE",ftInteger,0,true);
NewTable->FieldDefs->Add("SALE1",ftInteger,0,true);
NewTable->FieldDefs->Add("SALE2",ftInteger,0,true);
NewTable->FieldDefs->Add("SALE3",ftInteger,0,true);

//設定以 PLU_NO 欄位當 Primery Key
NewTable->IndexDefs->Clear();
NewTable->IndexDefs->Add("","PLU_NO", TIndexOptions() << ixPrimary << ixUnique);

NewTable->CreateTable();
NewTable->Free();
}

 
『列出目前桌面上所有視窗代碼』
主 Form 上放一個 Timer1 , ListBox1 , CheckBox1(其 Caption 設為"只列出可視視窗") , 然後在 OnTimer 中加入下列程式碼 , 就可在 ListBox1 列出目前桌面上所有視窗的 Caption , 類別名稱 , Hanlde 代碼
除錯監控時很好用

void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
int olditemindex=ListBox1->ItemIndex;
ListBox1->Items->Clear();
HANDLE hCurrentWindow = GetWindow(Handle, GW_HWNDFIRST);
char szText[254];
char className[254];
//GetClassName(hWnd,className,95);
//GetWindowText(hWnd,title,110);

String t;
String c;
while (hCurrentWindow != 0)
{
if ((GetWindowText(hCurrentWindow, szText, 255) > 0) &&
(IsWindowVisible(hCurrentWindow) || !CheckBox1->Checked))
{
GetClassName(hCurrentWindow,className,255);
t=szText;
c=className;
ListBox1->Items->Add(t+" --> "+c+" ; Handle = "+IntToStr((int)hCurrentWindow));
}

hCurrentWindow = GetWindow(hCurrentWindow, GW_HWNDNEXT);
}

if (olditemindex<=(ListBox1->Items->Count))
ListBox1->ItemIndex=olditemindex;
}

 
【 如何使用TChart?】
首先先在 Form 中放一個 Tchar 物件
什麼屬性都不要設
再放一個 button , 然後在 Button 的 OnClick 事件中加入下段程式碼
你就知道大概如何手動控制 Tchar 了 , 其他請舉一反三

void __fastcall TForm1::Button1Click(TObject *Sender)
{
Chart1->RemoveAllSeries(); // 清除Chart1上所有舊 Series

Chart1->View3D=false; // 不要 3D 立體
Chart1->Legend->Visible=false; // 不秀圖例說明

// 設定此 char Title 名稱
//(是 StringList 不是 String 所以不能用 Chart1->Title->Text="xxx" ...)
Chart1->Title->Text->Clear();
Chart1->Title->Text->Add("test");


// 動態宣告一個 THorizBarSeries 型態的 Series
// 其它的 Series 型態有哪些 請看 TChartSeries 之 Help
Series1=new THorizBarSeries(Chart1) ;
Series1->ParentChart=Chart1;

Series1->Marks->Visible=true; // 設定要提示說明
Series1->Marks->Style=smsValue; // 提示說明內容為 Label
//(註) Series1->Marks->Style 內容請參考 TSeriesMarksStyle 之 Help

Series1->SeriesColor=clBlue; // 設線條1為藍色,不設則自動給色

// 輸入假資料
int V; // Value
String L; //Label
for (int i = 1; i <= 8; i++)
{
V=i; // Series 值
L="V"+IntToStr(i); // 軸名稱
Series1->Add( V , L , clTeeColor );
}
}

 
『拆解字串段的自寫函式 _StringSegment()』
String A="ABCD,EFG,H,IJK,LM";
String B=_StringSegment(A , "," , 3); // 以逗號來做分隔 , 求第 3 段字串

所以 B 就等於 "H"
也可不用逗號做分隔 , 用您指定的其他符號做分隔


String __fastcall TForm1::_StringSegment(AnsiString Str , AnsiString Comma , int Seg)
{
if ((Str=="") || (Seg<1)) return "";

String C=Comma; if (C=="") C=",";

String s=Str;
String sTmp;
String r;
int iPosComma;

TStringList *TempList = new TStringList; // declare the list

TempList->Clear();
while (s.Pos(C)>0)
{
iPosComma = s.Pos(C); // locate commas
sTmp = s.SubString(1,iPosComma - 1); // copy item to tmp string
TempList->Add(sTmp); // add to list
s = s.SubString(iPosComma + 1,s.Length()); // delete item from string
}
// trap for trailing filename
if (s.Length()!=0) TempList->Add(s);

if (Seg > TempList->Count)
r="";
else
r= TempList->Strings[Seg-1];

delete TempList; // destroy the list object

return r;
}

 
『序列埠讀寫』
在 Windows 作業系統下(尤其是 NT & Win 2000),作業系統已禁止我們直接對硬體I/O Port做讀寫的動作,所以要控制 RS-232 Port,我們可利用檔案裝置的觀念來看待 RS-232 Port;這觀念源自 Unix 作業系統,所有的 I/O 週邊都可把它看成是一個檔案代碼,如 Com1 的檔案代號就是 “Com1”, Com2 的檔案代號就是 “Com2”,所以利用處理文字檔的觀念,我們就可以讀寫 RS-232 Port

序列埠的資料寫入

以程式碼的撰寫容易程度來看,RS-232 Port的「寫入」比「讀出」簡單,基本上有下列三種寫入方式,讀者可以看得出來,只要會處理文字檔的讀寫就看得懂本程式在做什麼,程式範例是假設有要秀出一字串到一週邊(如收銀機的「客戶抬頭顯示器」),除送出欲秀出的字串內容資料外,前導命令碼要加在資料之前,以便通知該週邊秀出方式的命令,本範例之週邊(假設接在 Com2)前導控制碼及資料格式假設為「Esc+’q’+’A’」+「欲秀出的字串」+「Return Code」,那我們來看看如何送出資料:

■ 使用傳統 C 的 文字檔處理語法

//-------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
String m="fprintf 測試"; //欲秀出的字串

FILE *outfile;
if ((outfile=fopen("COM2","w"))== NULL) return;

fprintf(outfile,"\x1bqA%s\r",m);

fclose(outfile);
}
//-------------------------------------------------------

■ 使用標準 C++ 的 文字檔串流(stream)處理語法

//-------------------------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
String m="ofstream 測試"; //欲秀出的字串

String s="\x1bqA"+m+"\r";
char *s1=s.c_str();
ofstream outfile("COM2"); if (!outfile) return ;
outfile << s1;
outfile.close();
}
//-------------------------------------------------------

■ 使用 Win32 API 之 CreateFile 函式(較複雜,但可控制BaudRate、ByteSize、Parity、StopBits等參數)很多市售的 RS-232 元件內部也都是用這種方式處理 RS-232資料輸出

//-------------------------------------------------------
void __fastcall TForm1::Button3Click(TObject *Sender)
{
String m="CreateFile 測試"; //欲秀出的字串

HANDLE hCom = CreateFile("COM2",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
0,
NULL );

if(hCom)
{
DCB dcb;
ZeroMemory(&dcb, sizeof(dcb));
dcb.DCBlength = sizeof(dcb);

dcb.BaudRate = 9600;
dcb.ByteSize = 8;
dcb.Parity = NOPARITY; // NOPARITY and friends are
dcb.StopBits = ONESTOPBIT; // #defined in windows.h

// Set the port properties and write the string out the port.
if(SetCommState(hCom,&dcb))
{
DWORD ByteCount;
String s="\x1bqA"+m+"\r";
char *msg=s.c_str();
WriteFile(hCom,msg,
strlen(msg),
&ByteCount,NULL);
}
CloseHandle(hCom);
}
}
//-------------------------------------------------------

註 : 以上只是本人研究心得 , 若要深入研究
BCB 現也已有 RS-232 控制之參考書籍可參考之

 
『 序列埠讀寫』
基本資料讀出方式

//---------------------------------------------------------------------------

#include
#pragma hdrstop

#include "Unit1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma link "ComPort"
#pragma resource "*.dfm"
TForm1 *Form1;
HANDLE ComPortHandle[10];
bool ComPortActive[10];

//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------

void __fastcall TForm1::FormActivate(TObject *Sender)
{
_ComPortOpen(1); //開啟 com port
}
//---------------------------------------------------------------------------

void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
_ComPortClose(1); //關閉 com port
}
//---------------------------------------------------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)
{
ShowMessage(_ComPortRead(1)); //測試讀出值 (如刷條碼機之讀出值)
}

//---------------------------------------------------------------------------
// 以下為副程式區
//---------------------------------------------------------------------------

bool __fastcall TForm1::_ComPortOpen(int ComPortID)
{
bool r=false;
if (ComPortActive[ComPortID]) return r;

String ComPortName="COM"+IntToStr(ComPortID);
ComPortHandle[ComPortID] = CreateFile( ComPortName.c_str(),
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
0,
NULL );

if(ComPortHandle[ComPortID])
{
DCB dcb;
ZeroMemory(&dcb, sizeof(dcb));
dcb.DCBlength = sizeof(dcb);

dcb.BaudRate = 9600;
dcb.ByteSize = 8;
dcb.Parity = NOPARITY; // NOPARITY and friends are
dcb.StopBits = ONESTOPBIT; // #defined in windows.h

// Set the port properties and write the string out the port.
if(SetCommState(ComPortHandle[ComPortID],&dcb))
{
r=true;
ComPortActive[ComPortID]=true;
}
}

return r;
}
//---------------------------------------------------------------------------
bool __fastcall TForm1::_ComPortClose(int ComPortID)
{
bool r=false;
if (CloseHandle(ComPortHandle[ComPortID]))
{
r=true;
ComPortActive[ComPortID]=false;
}

return r;
}
//---------------------------------------------------------------------------
String __fastcall TForm1::_ComPortRead(int ComPortID)
{
Char Buff[128];
int DataSize;
DWORD ByteCount;

//底下這一段只是為了找出 DataSize 值
DWORD ErrorFlags;
_COMSTAT ComPortStat;
ClearCommError(ComPortHandle[ComPortID],&ErrorFlags,&ComPortStat);

/* // lngSize 是有傳入要求指定的指定長度時
If ComPortStat.cbInQue > DataSize Then
DataSize = ComPortStat.cbInQue
Else
DataSize = lngSize
End If

*/

DataSize=ComPortStat.cbInQue;
if (DataSize<=0) DataSize=0;
//ShowMessage(IntToStr(DataSize));

ReadFile(ComPortHandle[ComPortID],Buff, DataSize,&ByteCount,NULL);
return String(Buff);
}
//---------------------------------------------------------------------------

void __fastcall TForm1::_ComPortClear(int ComPortID)
{
PurgeComm(ComPortHandle[ComPortID],PURGE_RXCLEAR);
}

//---------------------------------------------------------------------------

註 : 以上只是本人研究心得 , 若要深入研究
BCB 現也已有 RS-232 控制之參考書籍可參考之

 
『 鍵盤掃描碼處理』
在鍵盤按鍵被押下時,第一個會產生的是所謂的掃描碼(Scan Code),也就是硬體直接傳回的按鍵代碼,然後作業系統會把它轉換為虛擬碼(Virtual Key ),也就是用一些有意義的符號或代碼來代替之,以便應用程式能夠處理;例如一般鍵盤上都有兩個「Enter」鍵(第二個在鍵盤最右下角),它們在硬體上的掃描碼(Scan Code)一定不同,但虛擬碼(Virtual Key)都是 VK_RETURN (CharCode=13),一般在高階的程式應用中,大部分都只會針對虛擬碼(Virtual Key)做處理,這樣做的好處是可以將硬體隔離於應用程式之外。

在一個 Windows 程式裡,若有某鍵被按下,會有兩個訊息送到程式本身,一個稱之為 WM_KEYDOWN,幾乎鍵盤任何按鍵都會送出;而另一個訊息稱之為 WM_CHAR,只有在數值或字母按鍵被按下時才會送出;換句話說,若是按下了 「A」鍵,會同時得到 WM_KEYDOWN 及 WM_CHAR 兩個訊息,按下了 「F1」卻只會得到 WM_KEYDOWN 訊息。

OnKeyDown事件處理程序對應於 WM_KeyDown 訊息, OnKeyPress事件處理程序則對應於 WM_CHAR訊息 (OnKeyPress事件乾脆改名叫 OnChar 事件比較容易讓人理解),所以 OnKeyDown事件處理程序傳入的 key 變數是 WORD 型態,而 OnKeyPress 事件處理程序傳入的 key 變數是 char 型態。

由上面的介紹可以知道,要偵測所有的按鍵(包含文數字及所有功能鍵)要用OnKeyDown事件處理程序來做,偵測範圍會比較廣,但有個鍵例外,那就是「Tab」鍵;按下 Tab 鍵會轉移元件焦點(Focus),且不會觸發 OnKeyDown及OnKeyPress 事件;另外,OnKeyDown事件也只能偵測到鍵盤虛擬碼(Virtual Key)層級,它雖幾乎可偵測所有的按鍵,但它無法分辨按下的是左邊的 Shift或是右邊的Shift?或按下的是兩個 Enter 鍵中的哪一個?所以,要能分辨所有按鍵個體,必須能夠偵測到鍵盤的硬體掃描碼(Scan Code)層級。

■ 偵測鍵盤上所有按鍵個體

在 Form 中有個 OnShortCut 事件處理程序,抓取其傳入的 Msg 變數就可以完全偵測所有按鍵個體,更令人興奮的是它連 Tab 鍵都能抓得到,原因是從 Msg 變數中可以直接抓出鍵盤的硬體掃描碼(Scan Code);如欲抓取掃描碼可讀出 Msg.KeyData值,抓取虛擬碼(Virtual Key) 則可讀出 Msg.CharCode 值。以下整理出跟按鍵偵測有關的事件處理程序及其偵測範圍:

OnKeyPress 只能抓到數值或字母按鍵及 Esc鍵、空白鍵,但不含功能鍵(F1-F12)
OnKeyDown 能抓到所有的鍵(除 Tab 鍵)但不能分辨「對稱鍵」的不同
OnShortCut 能抓到所有的鍵(含 Tab 鍵)且能分辨「對稱鍵」的不同

註:「對稱鍵」指的是左右兩邊皆有的鍵,如「Enter」、「Shift」、「Ctrl」、「Alt」等等

 
『如何將 Ping 功能寫成一個函式』
使用方法 : bool Ping(String ip_address);

用法就這麼簡單 , 相關的函式用法
翻 MSDN 要傳入一大堆參數指標 ,
頭都昏了 , 因為我又不是要用來寫專業
的 Ping 程式 , 我大部份的用途都是在
Ping 區網內另一台主機在不在
我只要知 回傳值是 True 或 False
即可 , 根本不須知 ping 之 reply time 是多少 ....

不過這個函式有個缺點 , 若該 ip 不能反查出主機名稱
(就是網路芳鄰看到的名稱啦)
則就算 ip 存在 , 傳回值仍是 False
不過針對大部份的 Windows 系統電腦應都沒問題

不能反查出主機名稱的 ip 有
1. 區網內的路由器 - Router (ip 大部份為 x.x.x.254)
2. Linux 主機

###################################################
# 原始參考資料 http://rad.bytesandmore.de/index.htm?http://rad.bytesandmore.de/cpp/snipp/sc08012.php
# bruce0211@yahoo.com.tw 改寫 (2002/06/05)
#
# bool Ping(AnsiString ip_address) 函式
#
# 使用方法 :
#
# if (Ping("tfm002"))
# ShowMessage("true");
# else
# ShowMessage("false");
#
#####################################################

#####################################################
# ping.cpp 由此開始
#####################################################

#include
#pragma hdrstop
#include "Ping.h"

//---------------------------------------------------------------------------
// ip_address 可填入 ip 位置或主機名稱
// 但本函式有個 bug , 若該 ip 不能解析出主機名稱者
// 如 Router 或某些 Linux 主機 , 則回傳值仍是失敗 >_<
//---------------------------------------------------------------------------
bool Ping(AnsiString ip_address)
{
// ICMP.DLL laden:
HANDLE hIcmp = LoadLibrary("ICMP.DLL");
if(hIcmp == NULL)
{
return false;
}

// Zeiger auf die Funktionen besorgen:
PF_CMPCREATEFILE pfIcmpCreateFile = (PF_CMPCREATEFILE)
GetProcAddress(hIcmp, "IcmpCreateFile");
PF_ICMPCLOSEHANDLE pfIcmpCloseHandle = (PF_ICMPCLOSEHANDLE)
GetProcAddress(hIcmp,"IcmpCloseHandle");
PF_ICMPSENDECHO pfIcmpSendEcho = (PF_ICMPSENDECHO)
GetProcAddress(hIcmp,"IcmpSendEcho");

// Funktionszeiger prufen:
if (pfIcmpCreateFile == NULL || pfIcmpCloseHandle == NULL ||
pfIcmpSendEcho == NULL)
{
FreeLibrary(hIcmp);
return false;
}

// WinSock initialisieren
WSADATA wsaData;
int ilRetVal = WSAStartup(0x0101, &wsaData );
if(ilRetVal)
{
WSACleanup();
FreeLibrary(hIcmp);
return false;
}
// Check WinSock version
if(0x0101 != wsaData.wVersion)
{
WSACleanup();
FreeLibrary(hIcmp);
return false;
}

// Prufen, ob es sich bei der Zieladresse um IP-Adresse handelt und
// ggf. den die Adresse zum Namen ermitteln:
struct in_addr iaDest; // Struktur fur die Internet-Adresse
iaDest.s_addr = inet_addr(ip_address.c_str());
LPHOSTENT pHost; // Zeiger auf die Host Entry Struktur

if (iaDest.s_addr == INADDR_NONE)
pHost = gethostbyname(ip_address.c_str());
else
pHost = gethostbyaddr((BYTE *)&iaDest, sizeof(struct in_addr), AF_INET);

// 若該 ip 不能解析出主機名稱者
// 如 Router 或某些 Linux 主機 , 則回傳值仍是失敗 , 問題應出在這裡
// 但又不能拿掉 ....
if(pHost == NULL)
{
WSACleanup();
FreeLibrary(hIcmp);
return false;
}

// IP-Adresse kopieren
DWORD* pAddress = (DWORD*)(*pHost->h_addr_list);

// ICMP Echo Request Handle besorgen:
HANDLE hIcmpFile = pfIcmpCreateFile();

ICMPECHO icmpEcho; // ICMP-Echo Antwortbuffer
IPINFO ipInfo; // IP-Optionenstruktur

int ilTimeSum = 0; // Summe der Round Trip Time-Daten
int ilCount = 0; // Anzahl der Round Trip Time-Daten

for (int ilPingNo = 0; ilPingNo < 3; ilPingNo++)
{
// Default-Werte festlegen:
::ZeroMemory(&ipInfo, sizeof(ipInfo));
ipInfo.bTimeToLive = 255;
// ICMP Echo anfordern:
pfIcmpSendEcho(hIcmpFile, // Handle von IcmpCreateFile()
*pAddress, // Ziel-IP Addresse
NULL, // Zeiger auf den Buffer mit den
// zu sendenden Daten
0, // Buffergrosse in Bytes
&ipInfo, // Request-Optionen
&icmpEcho, // Antwort-Buffer
sizeof(struct tagICMPECHO), // Buffergrosse
5000); // Max. Wartezeit in Millisekunden

// Ergebnisse anzeigen:
iaDest.s_addr = icmpEcho.dwSource;

// falls Fehler aufgetreten:
if(icmpEcho.dwStatus)
{

break;
}
ilTimeSum += icmpEcho.dwRTTime;
ilCount++;
if(ilPingNo < 2) Sleep(200);
}

// Echo-Request File Handle schliessen:
pfIcmpCloseHandle(hIcmpFile);
// ICMP.DLL freigeben:
FreeLibrary(hIcmp);
// Winsock schliessen:
WSACleanup();

// Den Mittelwert aller Round Trip Times zuruckgeben:
//return ilRetVal = ilCount ? ilTimeSum/ilCount : -1;

if (ilRetVal = ilCount) return true;
else
return false;

}

###################################################
# ping.h 由此開始
###################################################

//----------------------------------------------------------------------------
#ifndef PingHPP
#define PingHPP
//---------------------------------------------------------------------------
#include

// Definition der IP-Optionenstruktur
typedef struct tagIPINFO
{
BYTE bTimeToLive; // Time To Live
BYTE bTypeOfService; // Type Of Service
BYTE bIpFlags; // IP-Flags
BYTE OptSize; // Grosse der Options Data Buffers
BYTE FAR *Options; // Zeiger auf Options Data Buffer
} IPINFO, *PIPINFO;

// Definition der ICMP-Echo Antwortstruktur
typedef struct tagICMPECHO
{
DWORD dwSource; // Zieladresse
DWORD dwStatus; // IP-Status
DWORD dwRTTime; // Round Trip Time in Millisekunden
WORD wDataSize; // Grosse des Antwort-Buffers
WORD wReserved;
void FAR *pData; // Zeiger auf die Antwort-Daten
IPINFO ipInfo; // Antwort-Optionen
} ICMPECHO, *PICMPECHO;

// Zeiger auf die Funktionen aus der ICMP.DLL deklarieren:
typedef HANDLE (WINAPI *PF_CMPCREATEFILE)(VOID);
typedef BOOL (WINAPI *PF_ICMPCLOSEHANDLE)(HANDLE);
typedef DWORD (WINAPI *PF_ICMPSENDECHO)(HANDLE,DWORD,LPVOID,WORD,
PIPINFO,LPVOID,DWORD,DWORD);

//---------------------------------------------------------------------------
bool Ping(AnsiString ip_address);
//---------------------------------------------------------------------------
#endif


另外 , 標準 WinInet 32 API 也有個 InternetCheckConnection()
可查某一主機是否連線 , 測試碼如下
但我試過 ,
目標 url 之主機必須啟動 http 或 ftp service 才會傳回 true
所以功能用途比前面找到的方法更狹隘 ....

char url[]="http://172.30.96.200/";
bool status = InternetCheckConnection(url,FLAG_ICC_FORCE_CONNECTION, 0 );

if (status)
ShowMessage("true");
else
ShowMessage("false");

 
『Set (集合型態) 的使用方法』
Set (集合型態) 是 Object Pascal 特有型態 , 在 BCB 中是用一個名為 Set 的類別來模擬
Set 型態 , 由於在 VCL 元件中有許多屬性都會利用到 Set 型態 , 所以在此介紹其使用方法

■ 加入集合元素

語法 : 集合型態原型 << 元素一 [<< 元素二] [<< 元素二] [...]

舉例 :

1. 在 MessageDlg 談出示對話盒中 , 若需要有兩個按鈕 "YES" & "NO"
則使用 TMsgDlgButtons() << mbYes << mbNo
完整範例如 if (MessageDlg("Delete Record?", mtConfirmation, TMsgDlgButtons() << mbYes << mbNo, 0) == mrYes)
DataModule3->Table1->Delete();

2. 我們要將某一元件之字型設為粗體 , 使用物件檢視器(Object Inspector)來設定很簡單 , 只要在
Font 屬性中展開 Style 次屬性 , 再將 fsBold 設為 true 即可

但在程式碼中如何做到以上動作呢 ? (以設定 Edit1 字型為粗體為例)
Edit1->Font->Style = TFontStyles() << fsBold;

若要同時設定其字型為粗體加底線 :
Edit1->Font->Style = TFontStyles() << fsBold << fsUnderline;

■ 判斷是否含有集合元素

□ 判斷 Edit1 目前是否為粗體字型

if (Edit1->Font->Style.Contains(fsBold))
ShowMessage("true");
else
ShowMessage("false");

□ 判斷 KeyDown 事件中是否帶有 Shift

void __fastcall TFm_POS1::FormKeyDown(TObject *Sender, WORD &Key,
TShiftState Shift)
{

if (Shift.Contains(ssShift))
{
......
......
}
}

□ 在做 StringGrid 變色特效時 , 判斷當在固定列或選擇列(藍色光棒列)則不于變色

void __fastcall TForm1::StringGrid1DrawCell(TObject *Sender, int ACol,
int ARow, TRect &Rect, TGridDrawState State)
{
// 如果是固定列或選擇列(藍色光棒列)則不于變色
if (State.Contains(gdFixed) || State.Contains(gdSelected)) return;

if (ARow==2) StringGrid1->Canvas->Font->Color=clRed; //字型變色
if (ACol==3) StringGrid1->Canvas->Brush->Color=clLime ; // 垂直 cell 背景變色
if (ARow==3) StringGrid1->Canvas->Brush->Color=clYellow; // 水平 cell 背景變色

// output the text
StringGrid1->Canvas->TextRect (Rect, Rect.Left, Rect.Top,StringGrid1->Cells[ACol][ARow]);
}

□ 註 : 根據測試 Contains() 中好像無法同時塞入兩個集合元素值 ...

■ 移除集合元素

同 "加入集合元素" 語法 , 但只要將 "<<" 改為 ">>" 即可

if (Edit1->Font->Style.Contains(fsBold))
Edit1->Font->Style = TFontStyles() >> fsBold;

 
『事件共用』
以設計一個小算盤為例
首先插入一個 Panel1 將其 Caption 清空
再插入十個 Button , 其 Caption 分別為 "0" - "9"
(當作計算機按鍵用)
然後在 Button1 之 OnClick 事件中寫入下列程式碼
void __fastcall TForm1::Button1Click(TObject *Sender)
{
Panel1->Caption=Panel1->Caption+((TButton *)Sender)->Caption;
}

其他九個按鍵之 OnClick 都指到 Button1 的 OnClick (共用)
如此十個按鍵在執行時都可以按出數字來
不用分別為十個 Button 寫十個 OnClick 事件程式碼 ......

 
『取得 IP 位址之函式 , String 型態』
String __stdcall GetHostIpAddress(void)
{
struct hostent *thisHost;
struct in_addr in;
char MyName[80];
char *ptr,*hostname;
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested=MAKEWORD(2,0);

err=WSAStartup(wVersionRequested,&wsaData);
if(err!=0)return "";
if(LOBYTE(wsaData.wVersion)!=2||HIBYTE(wsaData.wVersion)!=0)
{
WSACleanup();
return "";
}
if (gethostname(MyName,80)==SOCKET_ERROR) return "";
if (!(thisHost=gethostbyname(MyName))) return "";
hostname=thisHost->h_name;
memset((void*)&in,sizeof(in),0);
in.s_addr=*((unsigned long*)thisHost->h_addr_list[0]);
if (!(ptr=inet_ntoa(in))) return "";
WSACleanup();

return (String)ptr;
}

 
『動態連結檔簡易設計說明』

■ 特別說明

1. 動態載入部分 , DLL函式引用在參考資料中都只提到局部引用( 即在一個 BUTTON CLICK 事件中介紹如何載入及引用 DLL函式然後當場釋放 DLL)我光把他改成全域引用(在 FORM CREATE 載入 , Form Close 才釋放 , 這才符合真實應用環境 ) 就測了好久 , 在 MSDN 中還用到 TYPEDEF 來定義空函式指標 , 我測的結果根本不需要 , 就已可做到全域引用
2. 靜態載入部份 , 光參考 DLL 標頭檔 (*.H) 的巨集修改 (為了同時能適用 IMPORT 跟 EXPORT) , 不知是我資質愚昧還是參考資料表達的不詳細, 後來才搞懂其意義 , 但我也發現 , 不使用 DLL 之 *.H 標頭檔 , 不但沒關係 , 程式反而更簡潔有力 , 所以我的測試範例中根本不使用 DLL 的 *.H 標頭檔
3. 也許有些只是我的個人偏見 , 但在遇到瓶頸前我會再繼續研究修改


■ 使用樣板工具建立 DLL 雛形

□ 在 C++Builder 主選單中選擇「File」->「New…」出現下列樣板工具盒
□ 選擇 「DLL Wizard」出現對話選擇
□ 因為我們開發的 DLL 是要支援 C++Builder 本身程式用的,所以直接按「OK」即可,注意!有支援 VCL 的 DLL 只能給 C++Builder & Delphi 使用,若您的DLL不需用到 VCL 元件,而只有一些函數處理程序的話可將「Use VCL」勾勾拿掉,下一步其將自動產生DLL雛形程式碼;在這個空的DLL雛形程式碼內,筆者將自行加入三個自訂函式,以便測試這個 DLL 的功能,第一個函式傳入一個字元陣列然後用ShowMessage秀出這個字元陣列(函式本身沒有回傳值);第二個函式傳入兩個字元陣列,將這兩個字元陣列合併後回傳,回傳型態也是字元陣列;第三個函式傳入兩個整數,也是將這兩個整數相加後回傳,回傳型態是整數值;直接參考程式碼最明瞭,讀者應有能力看出哪些是系統產生的雛形樣板,哪些是作者加入的部分,這個 DLL完整程式碼如下:(專案名稱叫 MyDLL.bpr,程式碼名稱沿用系統內定之名稱 Unit1.pas)
□ 本 DLL 提供三個自寫副程式 Showit() 秀一段訊息 , CharAdd() 做字串相加 , IntAdd() 做整數相加

MyDLL 專案之完整 Unit1.pas
//-------------------------------------------------------

#include
#include
#include
#pragma hdrstop

extern "C" __declspec(dllexport) void __stdcall Showit(char *message);
extern "C" __declspec(dllexport) void __stdcall CharAdd(char *inchr1,char *inchr2 ,char *r);
extern "C" __declspec(dllexport) int __stdcall IntAdd(int a,int b);


#pragma argsused
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void* lpReserved)
{
return 1;
}
//-------------------------------------------------------
void __stdcall Showit(char *message)
{
ShowMessage(message);
}
//-------------------------------------------------------
void __stdcall CharAdd(char *inchr1,char *inchr2 ,char *r)
{
sprintf(r,"%s%s%c",inchr1,inchr2,'\0');
}
//-------------------------------------------------------
int __stdcall IntAdd(int a,int b)
{
return(a+b);
}
//-------------------------------------------------------

□ 程式說明

1.本 DLL原始碼 Unit.pas 不需有 Unit.h 標頭檔
2.本 DLL 編譯好後可同時適用於動態載入及靜態載入兩種呼叫方式 (下節介紹)
3.為了考慮共用性,函式命名方式加上 extern “C” 這個修飾詞來達到使用標準C的命名法,而不使用 C++的命名法,因為使用 C++的命名法會在編譯時在函式名稱後加上參數型態等註記,造成其他程式如 VC++、VB 無法使用的困擾
4.函式彙出或彙入之宣告,以32 位元系統之識別字 __declspec(dllexport) 及 __declspec(dllimport) 來取代舊 16 位元系統的識別字 _export 及 _import,因為舊 16 位元系統識別字在某些 C++編譯器下無法通過編譯
5.參數之指標堆疊以 __stdcall 型態為共通及最常用方式
6.在 DLL 中使用「String」參數型態非常麻煩跟無效率,所以建議都用字元陣列來處理字串問題 (如本例中第二個函式傳入兩個字元陣列,將這兩個字元陣列合併後回傳,其實就是用來代替字串相加的處理程序 )
7.到C++Builder之「Project」主選單將整個專案「Builde All Projets」,系統即自動產生 MyDLL.dll 及 MyDLL.lib ( 註:DLL 專案不能直接用來「RUN」)

■ 使用 DLL 檔 – 動態載入方式

□ 動態載入DLL之特點

1.DLL 內容若有修改或擴充,只要主程式所叫用到的函式本身之傳入傳出參數結構不變,重新編譯DLL即可,主執行檔 (EXE 檔)不用重新編譯
2.呼叫這個 DLL 的主執行檔之呼叫程序撰寫較麻煩,需由程式設計者自行引用或釋放 DLL 所佔用之記憶體
3.應用架構如下
(1) 先自行宣告會用到的空函式指標,以便載入DLL時套用DLL內原本之函式
(2) LoadLibrary(“MyDLL.dll”); //載入 DLL
(3) GetProcAddress(DLL之Handle,”函式名稱”); //找出 DLL 內函式之位址指標
(4) FreeLibrary(DLL之Handle) ; // 釋放 DLL
4.若該 DLL 不是自己寫的,可用 C++Builder 內附之 tdump.exe 工具查看該 DLL 內有哪些函式可被開放叫用
5.開始撰寫測試程式,畫面如下,目的為測試之前設計的三個函式功能

測試程式 PJ_3_2_2.exe 之 Unit 完整程式碼:
//-------------------------------------------------------
#include
#pragma hdrstop

#include "Unit1.h"
//-------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"

//先自行宣告會用到的空函式指標,以便載入DLL時套用DLL內原本之函式
void(__stdcall *Showit)(char *message);
void (__stdcall *CharAdd)(char *inchr1,char *inchr2 ,char *buff);
int(__stdcall *IntAdd)(int a,int b);

HINSTANCE DLLInst;

TForm1 *Form1;
//-------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//-------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
//於 Form 建立時順便載入 DLL (動態載入)
DLLInst=LoadLibrary("MYDLL.DLL");
(FARPROC&)Showit=GetProcAddress(DLLInst,"Showit");
(FARPROC&)CharAdd=GetProcAddress(DLLInst,"CharAdd");
(FARPROC&)IntAdd=GetProcAddress(DLLInst,"IntAdd");

// 如此,主程式內就有 Shoeit()、CharAdd()、IntAdd()三個
DLL函式可用了
}
//-------------------------------------------------------
void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
//於 Form 關閉時時釋放 DLL
FreeLibrary(DLLInst);
}
//-------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
//測試傳入字串陣列然後秀出
Showit("test");
}
//-------------------------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
//測試傳入兩字串陣列然後回傳合併結果
char buff[256];
CharAdd("ab","cd",buff);
ShowMessage(buff);
}
//-------------------------------------------------------
void __fastcall TForm1::Button3Click(TObject *Sender)
{
//測試傳入兩整數然後回傳相加結果
ShowMessage(IntAdd(1,2));
}
//-------------------------------------------------------

■ 使用 DLL 檔 – 靜態載入方式

□ 靜態載入DLL之特點

1.DLL 內容在修改或擴充時,就算主程式所叫用到的DLL函式本身之傳入傳出參數結構不變,但DLL內函式位置順序有變,不只是DLL檔,連整個主執行檔 (EXE 檔)都需重新編譯
2.主執行檔 (EXE 檔)編譯時需在專案檔(.bpr檔)中加入LIB 檔 (MyDLL.lib,當在製作MyDLL.dll 時,C++Builder 已同時產生),不像動態載入之主執行檔在編譯時根本不需DLL相關檔案存在 (只要執行時DLL檔存在就好了)
3.呼叫這個 DLL 的主執行檔之呼叫程序撰寫較簡單,系統自動管理或釋放 DLL 所佔用之記憶體
4.若主執行(EXE)檔編譯時要引用 MyDLL.h 標頭檔,此標頭檔還要大費周章經過一番修改(可參考其他 C++Builder 相關書籍),本書不使用引用 MyDLL.h 標頭檔的方法來編譯主程式,所以不用 MyDLL.h 標頭檔,減低應用複雜性
5.開始撰寫測試程式,同前例但使用靜態載入語法,目的同樣為測試之前設計的三個函式功能,您可發覺程式碼較動態載入方式簡單

測試程式 PJ_3_2_3.exe 之 Unit 完整程式碼:
//-------------------------------------------------------
#include
#pragma hdrstop

#include "Unit1.h"

//-------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;

//這個地方宣告方式跟動態載入不同
extern "C" __declspec(dllimport) void __stdcall Showit(char *message);
extern "C" __declspec(dllimport) void __stdcall CharAdd(char *inchr1,char *inchr2 ,char *r);
extern "C" __declspec(dllimport) int __stdcall IntAdd(int a,int b);
//以上三行也可拿掉並放在獨立之標頭檔內如 test.h 然後在本
//程式碼開頭 include "test.h"

//-------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//-------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
//測試傳入字串陣列然後秀出
Showit("test");
}
//-------------------------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
//測試傳入兩字串陣列然後回傳合併結果
char buff[256];
CharAdd("ab","cd",buff);
ShowMessage(buff);
}
//-------------------------------------------------------
void __fastcall TForm1::Button3Click(TObject *Sender)
{
//測試傳入兩整數然後回傳相加結果
ShowMessage(IntAdd(1,2));
}

//-------------------------------------------------------

注意使用靜態載入方式,主執行檔 (EXE 檔)編譯時尚需在專案檔(.bpr檔)中加入LIB 檔 (MyDLL.lib)


『多執行緒測試』
一般書上或找到的範例 , 皆將 Thread 寫在另一個 Unit (如 Unit2)中
本範例將整個程式寫在一起 , 不知是否正確 , 但 Run 起來沒問題

■ 測試目的

利用一個無窮迴圈來顯示目前時間 , 但卻不會影響 User 在 Memo 中編輯資料 ;
若此無窮迴圈不是放在 Thread 中 , 則整個 cpu 時間會被其佔用 ,
程式 Hold 住根本無法在 Memo 中編輯資料 ....

1.建一個主 Form
2.放一個 Button1 ,其 Caption 設為 "開始 Show Time Thread"
3.放一個 Button2 ,其 Caption 設為 "結束 Show Time Thread"
4.再放一個 Edit1 & 一個 Memo1

完整程式碼如下 :

//-------------------------------------------------------
#ifndef Unit1H
#define Unit1H
//-------------------------------------------------------
#include
#include
#include
#include
//-------------------------------------------------------
class TMyThread : public TThread
{
public:
__fastcall TMyThread(void);

private:
void __fastcall Execute(void);
void __fastcall ShowTime(void);
};
//-------------------------------------------------------
class TForm1 : public TForm
{
__published: // IDE-managed Components
TButton *Button1;
TButton *Button2;
TEdit *Edit1;
TMemo *Memo1;
void __fastcall FormCreate(TObject *Sender);
void __fastcall FormClose(TObject *Sender, TCloseAction &Action);
void __fastcall Button1Click(TObject *Sender);
void __fastcall Button2Click(TObject *Sender);
private: // User declarations
public: // User declarations
__fastcall TForm1(TComponent* Owner);

};
//-------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//-------------------------------------------------------
#endif


//-------------------------------------------------------
#include
#pragma hdrstop

#include "Unit1.h"
//-------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
TMyThread *MyThread;
//-------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//-------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
//
}
//-------------------------------------------------------
void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
if (MyThread) MyThread->Terminate();
}
//=======================================================
// TMyThread 程序實作區段開始
//=======================================================
__fastcall TMyThread::TMyThread(void):TThread(true)
{
Resume();
}
//-------------------------------------------------------
void __fastcall TMyThread::Execute(void)
{
FreeOnTerminate=true;
while (!Terminated)
{
// Methods and properties of objects in VCL can only be
// used in a method called using Synchronize()
Synchronize(ShowTime); // 要改變 VCL 物件內容必須透過 Synchronize 呼叫
// 不能把 Form1->Edit1->Text=TimeToStr(Now()) 寫在這裡
Sleep(1000);
}
}
//-------------------------------------------------------
void __fastcall TMyThread::ShowTime(void)
{
//本執行緒真正的動作寫在此
Form1->Edit1->Text=TimeToStr(Now());
}
//=======================================================
// TMyThread 程序實作區段結束
//=======================================================

void __fastcall TForm1::Button1Click(TObject *Sender)
{
MyThread = new TMyThread;
}
//-------------------------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
MyThread->Terminate();
}
//-------------------------------------------------------

 
『公司統一編號檢查函式』
bool __fastcall TForm1::Chk_unify(String d)
{
bool r=false;
String temp;
int x,y;

if (d.Length()!=8) return r;

String A[10];
String A7="";
String Key="12121241"; //統編檢查對應表
int sum1=0;
int sum2=0;

for (int i=1;i<=8;i++)
{
x=StrToInt(d.SubString(i,1));
y=StrToInt(Key.SubString(i,1));
temp=FormatFloat("00",x*y);
A[i]=IntToStr(StrToInt(temp.SubString(1,1))+StrToInt(temp.SubString(2,1)));
if (i==7 && A[i].Length()==2)
{
A7=A[i].SubString(2,1);
A[i]=A[i].SubString(1,1);
sum2=sum2+StrToInt(A7);
}
else
{
sum2=sum2+StrToInt(A[i]);
}

sum1=sum1+StrToInt(A[i]);
}

if ( ((sum1%10)==0) || ((sum2%10)==0) ) r=true;

return r;

}

 
『 刪除整個目錄的函式』
void __fastcall TForm1::_DelTree(String dir)
{
String d=dir;
String FileName;
TSearchRec SearchRec;

if (!DirectoryExists(dir)) return; // 要 #include "FileCtrl.hpp" 才有這個函式

if (d.SubString(d.Length(),1)!="\\") d=d+"\\";

if (FindFirst(d+"*.*", faAnyFile, SearchRec) == 0)
{
do { if (SearchRec.Attr == faDirectory)
{
if ((SearchRec.Name!=".") && (SearchRec.Name!=".."))
{
_DelTree(d+SearchRec.Name); //遞迴呼叫
RemoveDir(d+SearchRec.Name);
}
}
else
{
FileSetAttr(d+SearchRec.Name,faArchive);
DeleteFile(d+SearchRec.Name);
}
} while (FindNext(SearchRec) == 0);

FindClose(SearchRec);

}

RemoveDir(d);

}

 
『參數檔 (INI 檔) 處理』
INI 檔應用的歷史從 Windows 3.1就已存在,雖然它也是文字檔的一種,但它利用「節區(Section)」及「識別字(KeyWord)」觀念,類似資料庫的索引,所以用來當作少量變數資料的儲存器非常適合,除變數管理容易外尚可做程式流程控制,市售套裝軟體有用 ini 檔來控制其軟體版本例子,其主執行檔可能只有一個,但依 INI 檔設定可開放其全功能版(豪華版)與非全功能版;INI檔案 Size 雖然有 64k的限制,但已足夠存放上百個變數資料;在 32 位元系統上,正統取代 INI 機能的方法為使用 Windows Regist,但為了儲存Windows 作業系統本身的資訊,Regist已夠龐大,如果我們每個應用程式又去 Regist內挖一塊空間當作自己儲藏變數或參數的地方,會讓 Regist變得更龐大更複雜,所以筆者還是建議每個程式都使用自己的INI檔來儲存自己的變數,況且 INI檔不會被淘汰,連 Linux Kylix 下都看得到其蹤影

■ 應用實例

假設程式中需要紀錄目前發票號碼(invo_no)、交易序號(tran_no),而每次交易完都要將這兩個變數值加1,若程式從頭到尾都不關機,那沒有問題,程式會記錄目前的發票號碼及交易序號已排到幾號;但若程式中途有關機後,下次開機,程式怎知發票號碼及交易序號已排到幾號?所以除了將變數資料存於資料庫中外,更簡單的方法就是使用INI檔來記錄變數值,在程式重新啟動時再將變數值取回,所以該INI檔內容可能長得如下:

[Varible]
invo_no=00000005
tran_no=3

其中,用中括號刮起來的Variable就是「節區(Section)」名稱(可自訂),而其內的 INVO_NO、TRAN_NO則稱為「識別字(KeyWord)」(可自訂);不可有相同名稱的兩個節區(Section),但不同的節區中可有相同名稱的識別字(KeyWord),取用時按節區(Section)索引分開取用,如下圖:

[Varible]
invo_no=00000005
tran_no=3

[Varible_2]
invo_no=00000007
tran_no=5

■ 程式實例

接下來我們介紹使用 INI 檔來儲存或取回變數的方法,在程式中要使用 INI 檔有幾個步驟:

1.程式開頭必須 #include "inifiles.hpp"
2.程式中首先需建立一個 INI 物件以對應實際的 INI 檔,不用時再釋放這個 INI 物件,通常 INI 檔實 體與主執行檔存在同一路徑,甚至檔名也和主執行檔相同,但副檔名則為 *.ini
3.使用 INI 物件的某些方法(Method)來存取變數,取用的方法依變數型態有下列幾種方法(假設這個 INI 物件名為 MyIni)
MyIni->ReadInteger(節區名,識別字,整數Default值);
MyIni->ReadString(節區名,識別字,字串Default值);
MyIni->ReadBool(節區名,識別字,布林Default值)
4.寫入的方法依變數型態也有下列幾種方法
MyIni->WriteInteger(節區名,識別字,欲寫入之整數變數值);
MyIni->WriteString(節區名,識別字,欲寫入之字串變數值);
MyIni->WriteBool(節區名,識別字,欲寫入之布林變數值)

以下為一完整使用INI程式碼的範例
//-------------------------------------------------------
#include
#pragma hdrstop

#include "Unit1.h"
#include "inifiles.hpp" //要 include 這個東西才有 TIniFile 類別
//-------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
TIniFile *MyIni;
//-------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//-------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
//建立 ini 物件及實體存放路徑
MyIni = new TIniFile(ChangeFileExt( Application->ExeName, ".ini" ) );

//讀取變數(若變數值不存在則用自動使用 Default 值)
Left=MyIni->ReadInteger( "Form","Left",100);
Caption=MyIni->ReadString( "Form","Caption","Default Caption");
}
//-------------------------------------------------------
void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
//將變數寫入 ini 物件
MyIni->WriteInteger("Form","Left",Left);
MyIni->WriteString("Form","Caption",Caption);

// 釋放 ini 物件
delete MyIni;
}
//-------------------------------------------------------

■ INI 物件的生命週期

在上面的程式碼中,可以看見我們把 INI 物件的建立寫在 FormCreate 事件中,而釋放 INI 物件則寫在 FormClose 事件中;事實上,變數真正寫到實體檔案中,是在INI 物件釋放的時候(也就是類似關閉檔案動作);平常則只是暫存於記憶體中,如此可減少磁碟 I/O 動作,但若程式不正常關閉,則INI 物件來不及將暫存於記憶體中的變數存檔,會導致欲存檔的變數遺失;所以為保險起見,我們可在準備寫入變數的時候才去建立 INI 物件,存入後馬上釋放 INI 物件以確保寫入實體檔案中,但這樣卻會增加磁碟I/O讀寫頻繁,其中的斟酌端看應用方向的特性。

//-------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
TIniFile *MyIni;
MyIni = new TIniFile(ChangeFileExt( Application->ExeName, ".ini" ) );

MyIni->WriteInteger("Form","Left",Left);
MyIni->WriteString("Form","Caption",Caption);

delete MyIni;
}
//-------------------------------------------------------

 
『本文檔 ( TEXT檔) 處理』
本文檔 (TEXT 檔,或稱文字檔)的應用非常廣泛,最常見的應用就是不同平台或系統間的資料交換動作,以本文檔作為溝通橋樑;處理本文檔的讀寫有使用傳統 C 的語法及標準 C++ 的文字檔串流(stream)處理語法,雖然在 32位元平台下建議使用標準 C++ 的文字檔串流處理語法,但使用傳統 C 的語法有個好處,那就是 C 語法中 fprintf 及 fscanf 可以「格式化」輸出或輸入的字串的內容。

■ 本文檔的輸出

□使用傳統 C 的處理語法

1.要先於程式開頭 #include "stdio.h"
2.append (附增)模式之宣告
outfile=fopen("C:\\TEST.TXT","a+");
3.overwrite(覆寫)模式之宣告
outfile=fopen("C:\\TEST.TXT","w");

範例:
//-------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
String m="fprintf 測試"; //欲輸出的字串行
FILE *outfile;
if ((outfile=fopen("C:\\TEST.TXT","w"))== NULL) return;

fprintf(outfile,"%s\n",m);

fclose(outfile);
}
//-------------------------------------------------------

□使用標準 C++ 的文字檔串流(stream)處理語法

1.要先於程式開頭 #include "fstream.h"

2.使用 ofstream 物件作為檔案串流輸出物件

3.append (附增)模式之宣告
ofstream outfile("c:\\test.txt" , ios::app);

4.overwrite(覆寫)模式之宣告 (Default)
ofstream outfile("c:\\test.txt" , ios::trunc);

ofstream outfile("c:\\test.txt")

範例:

 //-------------------------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
  String m="ofstream 測試"; //欲輸出的字串行
  ofstream outfile("C:\\TEST.TXT");
  if (!outfile) return ;

  outfile << m.c_str();
outfile.close();
}
//-------------------------------------------------------

■ 本文檔的讀入

□ 使用傳統 C 的處理語法

由於 fscanf() 很難用 , 而且遇到空白就當作結束 , 所以拿來讀文字檔會很吃力
這裡省略不介紹


□ 使用標準 C++ 的文字檔串流(stream)處理語法

1.要先於程式開頭 #include "fstream.h"
2.使用 ifstream 物件作為檔案串流讀入物件
3.事先宣告一足夠容納讀入之每行長度的 buffer 當讀入緩衝區,若讀入之行長度超出緩衝區大小,有可能導致程式死當;故雖然在大部分的情況下,我們都可預測每行的讀入長度,但預防有例外狀況讀入某行長度特長,故筆者習慣將緩衝區大小設為 65535
4.使用 while (!infile.eof()) 迴圈來作逐行讀入
5.若於最後一行總是會讀入空白行時,可改用 while (!infile.getline(buff,sizeof(buff)).eof())迴圈來作逐行讀入

範例:

 //-------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
char buff[65535];
  String temp;
ifstream infile;
  infile.open("c:\\test.txt");
  if (!infile) return ;
while (!infile.eof())
{
infile.getline(buff,sizeof(buff));
temp = buff;
if (Trim(temp)=="") continue;

// 讀入之 buff 變數的應用例
Memo1->Lines->Add(temp);

}

infile.close();
}
//-------------------------------------------------------

 
『Tag & Hint 屬性的妙用』

■ 每個 VCL 物件幾乎都有 Tag 屬性 , Tag 可用來放一個 整數值 , 由於此屬性值不會影響物件外觀,或其他物件行為 , 所以通常都用來當作"藏值"的地方 ; 利用事件共用 , 可將一堆相同的物件集合分別藏不同的值 , 然後共用同一事件後再例用分離法 , 將此值取出 ; 如之前介紹的計算機按鈕 , 設計計算機程式時有十個相同的按鈕 0 - 9, 但不用為十個按鈕寫十個 OnClick , 共用同一事件但要能分辨彼此按鍵代表不同的值 ; 但今天若應用在別的地方 , 在 POS 系統觸控螢幕操作畫面 , 按下不同按鈕得出不同的條碼值 , 然後再去資料庫中搜尋商品 , 由於條碼值太長 (13 位數) , 最好是當字串處理 , 那該將每個按鈕所代表不同的條碼值藏在哪呢 ? 筆者第一個想到的就是 Hint 屬性 , 由於面板商品不需 Hint , Hint 設定後只要 ShowHint 不要設為 true , Hint 就不會跑出來 , Hint 就可拿來當作物件本身偷藏"字串"值的地方 , 以下是範例介紹

■ 動態產生"面板商品"按鈕的範例

□ 須使用按鈕 或是"看起來"像按鈕,且能按下去的元件
□ 要求條件 : Caption 要有兩行 , 按鈕可變顏色 , 要有立體感 , 但不能聚焦(Focused)
□ 測試
使用 Panel , 可變色 , 可設立體感 , 但 Caption 不能折行
使用 TSpeedButton 可設立體感 , Caption 可折行 , 但不能變色
使用 Label , 可變色 , 可折行 , 但不能設立體感
最後方案 使用 Label + Bevel(模擬按鈕立體感,及被按凹下去的感覺)
....當然也可使用 Label + Panel

□ 開始實做
1.首先開一個約 800 x 600 的 Form
2.再放一個 800 x 400 左右的 Panel (名叫 Panel1) 在 Form 中間偏下的部位
3.在中間偏上的部分放兩個 button , 一個用來產生動態物件 , 另一個用來釋放動態物件
4.按下"產生動態物件"按鈕後會在 Panel1 上產生一個 8 x 6 的按鈕陣列 , 重點是每個按鈕按下去所得的值都不相同 , 可用來藏"條碼值" , 然後做後續應用 ....
5.完整程式碼如下

//---------------------------------------------------------------------------
#include
#pragma hdrstop

#include "Unit1.h"
#include "fstream.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
TLabel *lb[49]; // 宣告動態 Label 物件陣列 , Global 變數
TBevel *bv[49]; // 宣告動態 Bevel 物件陣列 , Global 變數
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
// 產生動態物件的按鈕事件
//---------------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
//須使用不能聚焦元件....
//使用 Panel , 可變色 , 可設立體感 , 但 Caption 不能折行
//使用 TSpeedButton 可設立體感 , Caption 可折行 , 但不能變色
//使用 Label , 可變色 , 可折行 , 但不能設立體感
//最後方案 使用 Label + Bevel(模擬立體感)

int x=0; int y=1;
for (int i=1;i<=48;i++)
{
x++;
if (x>8)
{
x=1;
y++;
}

bv[i]= new TBevel(this);
bv[i]->Parent=Panel1;
bv[i]->Width=98;
bv[i]->Height=55;
bv[i]->Style=bsRaised;
bv[i]->Left=(x-1)*(bv[i]->Width+1)-0;
bv[i]->Top=(y-1)*(bv[i]->Height+1)-0;

lb[i]= new TLabel(this);
lb[i]->Parent=Panel1;
lb[i]->OnMouseDown=&FF_Plu_MouseDown; //指派動態物件所需的事件
lb[i]->OnMouseUp=&FF_Plu_MouseUp; //指派動態物件所需的事件
lb[i]->AutoSize=false;
lb[i]->Alignment=taCenter;
lb[i]->Width=96;
lb[i]->Height=53;
lb[i]->Left=bv[i]->Left+1;
lb[i]->Top=bv[i]->Top+1;

lb[i]->Tag=i;
lb[i]->Hint="條碼值 : 47100000000"+FormatFloat("00",i);

lb[i]->Caption="\n"+IntToStr(i)+"\n"+"test";
switch (y)
{
case 0 : lb[i]->Color=clBlack; break; //黑
case 1 : lb[i]->Color=0x000080FF; break; //棕
case 2 : lb[i]->Color=0x00FF80FF; break; //紅
case 3 : lb[i]->Color=0x004080FF; break; //橙
case 4 : lb[i]->Color=0x0080FFFF; break; //黃
case 5 : lb[i]->Color=0x0080FF80; break; //綠
case 6 : lb[i]->Color=0x00FF8000; break; //藍
case 7 : lb[i]->Color=0x00FF0080; break; //紫
case 8 : lb[i]->Color=clBtnFace; break; //灰
case 9 : lb[i]->Color=clWhite; break; //白
}

}

}
//---------------------------------------------------------------------------
// 自行寫給動態物件用的 OnMousDown() 事件
//---------------------------------------------------------------------------
void __fastcall TForm1::FF_Plu_MouseDown(TObject *Sender,
TMouseButton Button, TShiftState Shift, int X, int Y)
{
//按下 Label 時將 Label 中的 Tag 值取出 ,
//再呼叫對應 index 之 Bevel 物件做出外框 "按下"的感覺
bv[((TLabel *)Sender)->Tag]->Style=bsLowered;

//按下 Label 時改變 Label 位置以便有"按下"的感覺"
((TLabel *)Sender)->Left++;
((TLabel *)Sender)->Top++;
((TLabel *)Sender)->Width--;
((TLabel *)Sender)->Height--;

}
//---------------------------------------------------------------------------
//自行寫給動態物件用的 OnMousUp() 事件
//---------------------------------------------------------------------------
void __fastcall TForm1::FF_Plu_MouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
//放開 Label 時將 Label 中的 Tag 值取出 ,
//再呼叫對應 index 之 Bevel 物件做出外框 "浮起"的感覺
bv[((TLabel *)Sender)->Tag]->Style=bsRaised;

//放開 Label 時改變 Label Caption 位置以便有"浮起"的感覺"
((TLabel *)Sender)->Left--;
((TLabel *)Sender)->Top--;
((TLabel *)Sender)->Width++;
((TLabel *)Sender)->Height++;

ShowMessage(((TLabel *)Sender)->Hint); // 將被按下的 Label 偷藏的字串變數帶出以便應用

}

//---------------------------------------------------------------------------
//刪除動態物件的按鈕事件
//---------------------------------------------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
for (int i=1;i<=48;i++)
{
delete lb[i];
delete bv[i];
}
}

 
『固定 TMediaPlayer 元件撥放影片的 Size』
■前言
我們通常拿 TMediaPlayer 來撥放媒體影片 , 而 TMediaPlayer 的 Display 屬性可設定影片要播放在哪個地方 , 如可以指定播放在某一 Panel 上 (哪些元件可供做播放平台可自行測試), 或是都不設 , 讓程式執行時系統自動另開一 Form 以供播放之用 , 但影片本身播放時的高度跟寬度有大有小 , 如何固定 Size 呢 ? TImage 有一屬性叫做 Stretch 可將影像調整跟 TImage 本身大小一樣 ; 但出乎意料之外的 , TMediaPlayer 的 Display 屬性居然不能設定播放平台為 TImage 元件 , 由於客戶的需求是不管影片本身解析度如何 , 播放出來都要為 800 x 600 Size , 我研究了好久才試出來 , 只要在播放之前指定播放 Size 為所要播放的元件平台 Size 即可

■實作 : 如要將影像播放在 Panel1 上
1.將 Panel1 Size 拉大為 800 x 600
2.在 Play() 之前設定程式碼 :

  TRect r;
  r.Left=0;
  r.Top=0;
  r.Right=Panel1->Width ;
  r.Bottom=Panel1->Height;
  MediaPlayer1->DisplayRect=r;
MediaPlayer1->Play();

則以後不管任何影片播放時 , 長寬度大小一定為 800 x 600

■補充 : 將影片連續播放
1.將 TMediaPlayer 之 AutoRewind 屬性設為 true
2.在 TMediaPlayer 之 OnNotify 事件中加入程式碼如下 :


void __fastcall TForm1::MediaPlayer1Notify(TObject *Sender)
{
  if (MediaPlayer1->NotifyValue == nvSuccessful)
      MediaPlayer1->Play();
}

 
『如何判斷關閉程式的動作』
■前言
有些常駐程式須判斷被關閉的時機,
讓 user 不可以任意關掉程式
但卻允許 user 關機
所以要判斷使用者是按了程式右上角 [X] 按鈕
或是 使用者按了螢幕左下角 [關機] 指令
下面是簡單判斷的程式碼...

//---------------------------------------------------------------------------
#include
#pragma hdrstop

#include "Unit1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------

void __fastcall TForm1::FormCreate(TObject *Sender)
{
WindowProc=MyWndProc; //攔截訊息
}
//---------------------------------------------------------------------------
void __fastcall TForm1::MyWndProc(TMessage &Message)
{
 if (Message.Msg == WM_SYSCOMMAND)
 {
  if (Message.WParam== SC_CLOSE) //若Message為 Close Window
  {
   ShowMessage("使用者按了程式右上角 [X] 按鈕");
  }
 }

 if (Message.Msg == WM_ENDSESSION)
 ShowMessage("使用者按了螢幕左下角 [關機] 指令");

 //將訊息還給 Form 原來處理程序,否則只是收到關閉指令,但不會真正執行關閉動作
 WndProc(Message);
}

■補充
通常我們就在以上兩個時機點帶入一個旗標,
到FormCloseQuery()事件中再決定關機與否 ,
但根據我的測試,WM_ENDSESSION 觸發的時機會比 FormCloseQuery() 事件晚到
(WM_SYSCOMMAND 則不會) ,
也就是說錯過了關機判斷時機後,才會有 WM_ENDSESSION 訊息被收到 ,
這時可改判斷是否收到 WM_QUERYENDSESSION 來代替判斷 WM_ENDSESSION ,
程式流程就會正確判斷無誤

 
『 如何將自己的 AP 自動被 Focused』
■前言
最近在寫 POS 第二片螢幕做媒體播放時,媒體播放由於是呼叫另一個獨立執行檔(因為不想跟 POS 主程式混在一起寫),發覺焦點會跑到媒體播放程式上,本想說用以前的知識,先記住主程式的 Handle , 再用下列方法中的幾個方法
ShowWindow(Handle)
BringWindowToTop(Handle)
SetForegroundWindow(Handle)
SetActiveWindow(Handle)
Application->BringToFront()
就可將焦點自動帶回主程式
沒想到主程式是會被帶到 Top 沒錯
但似乎不能自動被 Focused
(按鍵主控權仍然在媒體播放程式上)
且執行上述 API 後
底下工作列,我主程式的小圖示會一閃一閃
表 API 有作用 , 但 就是一定要人用 MOUSE 去點一下
才會被 Focused (鍵盤輸入才會回到主程式)
但我的 POS 程式是無滑鼠系統,
鍵盤也是 POS 專屬鍵盤(沒有 ALT+Tab 可以切換程式焦點)
POS 程式上遺失焦點是件很嚴重的事
後來問人終於得到答案,在此分享:
在主程式中設個 Timer ,隨時檢查焦點並將焦點設回到主程式中

void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
 SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, 0, SPIF_SENDCHANGE);
 SetForegroundWindow(Application->MainForm->Handle);
}

沒有留言:

張貼留言