前言:

四年前在delphi ktop有人提到看盤軟體配合DDE技術,在用delphi實作時發生了亂碼加在後面的情形。http://delphi.ktop.com.tw/board.php?cid=30&fid=71&tid=97629

當時沒時間,只是大致上猜其原因。四年後的今天,又有一位朋友說他找到如何解的方式。於是讓我再度燃起追其真正的原因。

 

原因:

TDDEClientItemText屬性在某些情形下會得到亂碼加在資料的後面。原因追到VCL源碼去後發現,Text的值與下面的函式有關:

procedure TDdeCliItem.StoreData(DdeDat: HDDEData);

var

Len: Longint;

Data: string;

I: Integer;

begin

if DdeDat = 0 then

begin

   RefreshData;

   Exit;

end;

 

Data := PChar(AccessData(DdeDat, @Len)); //這裡就是問題的所在!

if Data <> '' then

begin

   FCtrl.Lines.Text := Data;

   ReleaseData(DdeDat);

   if FCliConv.FormatChars = False then

   begin

     for I := 1 to Length(Data) do

       if (Data[I] > #0) and (Data[I] < ' ') then Data[I] := ' ';

     FCtrl.Lines.Text := Data;

   end;

end;

DataChange;

end;

 

問題在Data := PChar(AccessData(DdeDat, @Len)); 這行上。其中,AccessData這個函式又叫了Win32APIDdeAccessData(DdeDat, pDataLen); 這個api回傳的是一個指向Byte型別的指標。經強轉為PChar (指向Char)的指標後,再透過內delphi的內建的AnsiString建構式把PChar指的字串變成AnsiString。至此,感覺好像沒錯… 但深入的思考,若傳回的指標的結尾null 值放在不正確的位置上,就會造成附加一些亂碼出現。然而,有可能null值的位置錯了嗎? 因為值的來源是dde server上傳來的,問題追入dde server 相關的api上。經了解,它與win32api裡的DdeCreateDataHandle 這個函式有關。查msdn:

HDDEDATA WINAPI DdeCreateDataHandle(

_In_     DWORD idInst,

_In_opt_ LPBYTE pSrc,

_In_     DWORD cb,

_In_     DWORD cbOff,

_In_opt_ HSZ hszItem,

_In_     UINT wFmt,

_In_     UINT afCmd

);

 

其中,第三個參數,是關鍵,它描述出要填入內容的長度為何。仔細再看第三參數的說明:

The amount of memory, in bytes, to copy from the buffer pointed to by pSrc. (include the terminating NULL, if the data is a string).

哇重點來了啦! 有沒有,當server把資料傳入時,若是傳字串,長度要「自己加1」因為要含null這個結尾。

 

於是假設寫server的那個人,忘了這件事… 我覺得這種事常常會發生的。那麼就會造成null值沒被放入記憶區塊。舉例來說,若要放入           ABC這三個字在memory上。假設該memory上目前的值是一堆亂的東西,比如說長這樣:

31 32 33 34 35 36 37 38 00 31 32(十六進位),若當它字串來看是 '12345678'

當把ABC放入 (其實是copy上去)那這區塊會變成

) 41 42 43 34 35 36 37 38 00 31 32 (當字串來看是ABC45678,若指定3的長度)

或是

) 41 42 43 00 35 36 37 38 00 31 32 (當字來看就是ABC,若指定4的長度,因null值有進去啦)

 

到目前為止,似乎命運操在寫server 程式人的手上,但沒那麼悲觀。再回首看上面講的DdeAccessData(DdeDat, pDataLen);這個api。第二個參數是那道光! 它指示出資料本身是多長,它呼應了DdeCreateDataHandle的第三個參數!! 假始,以剛的例子來說,server的實作是把ABC用長度3copy進記憶區塊中,那麼這個pDataLen就會是3。有這個值就有救了!

 

跳離一下上面的論述。因朋友提到他用TDdeClientConv.RequestData的方法取值就正確! 於是我追入源碼看:

function TDdeClientConv.RequestData(const Item: string): PChar;

var

hData: HDDEData;

ddeRslt: LongInt;

hItem: HSZ;

pData: Pointer;

Len: Integer;

begin

Result := nil;

if (FConv = 0) or FWaitStat then Exit;

hItem := DdeCreateStringHandle(ddeMgr.DdeInstId, PChar(Item), CP_WINANSI);

if hItem <> 0 then

begin

   hData := DdeClientTransaction(nil, 0, FConv, hItem, FDdeFmt,

     XTYP_REQUEST, 10000, @ddeRslt);

   DdeFreeStringHandle(ddeMgr.DdeInstId, hItem);

   if hData <> 0 then

   try

     pData := DdeAccessData(hData, @Len); // 從這裡開始看

     if pData <> nil then

     try

       Result := StrAlloc(Len + 1); // 哇,有聰明,會用len + 1

       Move(pData^, Result^, len);   // data is binary, may contain nulls

       Result[len] := #0; // 自己在最後面塞 null 值,讚啦!

     finally

       DdeUnaccessData(hData);

     end;

   finally

     DdeFreeDataHandle(hData); // 到這裡結束關鍵

   end;

end;

end;

 

是不是? 更加驗證了我開始的推論!! 同樣都是在取dde server送來的資料,竟不同的人員(絕對是不同的borland工程師,寫法就是不同)用不同的寫法。RequestData方法的實作是很嚴僅的!! 它考慮了server 的實作人員可能會忘了把null的長度也算進去啦! TDdeCliItem.StoreData的實作人員就不再乎,他認為那是寫server的人員自己該注意的? 事實上,若你DDE server是用delphi寫的,嘿~~ borland的人員是有加null的長度的! 類似如下:

 

hszCmd := DdeCreateDataHandle(ddeMgr.DdeInstId, Cmd, StrLen(Cmd) + 1,

   0, 0, FDdeFmt, 0);

所以,用delphi寫的DDE server來驗本題目,是完全不會錯! 這可能也是實作TDdeCliItem.StoreData函式的人員測式的時候總不會發現問題,於是就這樣不嚴僅下去…

 

解法:

了解以上的來龍去脈後,解就不難,我修正如下:

procedure TDdeCliItem.StoreData(DdeDat: HDDEData);

var

Len: Longint;

Data: string;

I: Integer;

begin

if DdeDat = 0 then

begin

   RefreshData;

   Exit;

end;

 

Data := PChar(AccessData(DdeDat, @Len)); //這裡就是問題的所在!

if Data <> '' then

begin

        SetLength(Data, Len); //加入這行應該就可以搞定! 我沒delphi可測。

   FCtrl.Lines.Text := Data;

 

至於改好後,要怎麼來變更delphilibrary ? 這又是另一個題目…我認為應該是將ddeman.pas檔照上面改好後,在delphi裡,用install component的功能,選ddeman.pas檔,然後compile,然後會出錯,但沒關係,會得到ddeman.dcu,然後把這個檔去蓋delphi安裝目錄下的那個。請自行試看看。因我沒有delphi,我都是用c++ builder,故無法驗證後再保證沒錯啦! 請見諒!

 

結論:

不知這樣的結果算不算是bug? 假始寫server的人,用vc++寫,也注意到要含null一起copy到記憶體區塊,那就不會出事。但,最佳的解法是依DdeAccessData(hData, @Len); 中的len來決定一個正確的字串,而不是依null值的所在位置。(null值可能是在正確字串後的任何一個地方出現的)。我會找時間去Embarcadero report這個bug?,希望在未來的版本得到修正!

文章標籤
創作者介紹

蕭沖的書房

aftcast 發表在 痞客邦 PIXNET 留言(0) 人氣()