Delphi - procedure fails to complete, but works fine with "showmessage" between.? - delphi

I'm not quite sure how to even ask this question, since I don't know whether it is related to the execution time, application process.message procedure or anything else.
I'm having (for me) weird situations, where the procedure fails to run and raises system exception on run, while it runs completely flawless if I put "showmessage" there in between (which I put so that I could quickly see what's going on in between. I prefer that way over watches somehow...).
I'm not sure whether the code matters or not, but I'll give it below:
procedure LoadSettings;
var SettingsBuffToLoad: TStringList;
begin
SettingsBuffToLoad:=TStringList.Create;
Encoding:=TEncoding.ANSI;
SettingsBuffToLoad.LoadFromFile('bin/settings.txt', Encoding);
// showmessage(settingsbufftoload.Strings[0]);
SettingsBuffer:=Decode(SettingsBuffToLoad);
// showmessage(settingsbuffer.Strings[0]); //decode
end;
The Decode procedure is declared as external and is read from the dll.
If I just remove those "/" , so that it becomes the code instead of comment, it works just fine. However, set as you see now, it raises exception, but after the procedure is already done. (the debugger last break point is stopped at "end;", after continuing however it raises exception instead of showing the form; this procedure is called as the last thing in FormCreate procedure.
Is there anything that has to do with the timing, which ShowMessage solves, or...? :/
Update:
The decode functions, as asked:
this is how it's declared, right above of the implementation and variables of the form:
function Decode(Buff: TStringList): TStringList; StdCall; external 'bin\settings.txt';
And this is in the dll:
function Decode(Buff: TStringList): TStringList; export;
var
t, u, h: integer;
s: String;
begin
DecodeBuffer.Clear;
DecodeBuffer:=Buff;
for h := 0 to DecodeBuffer.Count-1 do
begin
s := DecodeBuffer.Strings[h];
t := Length(s);
if t > 0 then
begin
for u := 0 to t-1 do
begin
s[u+1] := DecodeChar(s[u+1], (h mod 5) + 1);
end;
DecodeBuffer.Strings[h] := s;
end;
end;
Result:=DecodeBuffer;
end;
This code was discussed in a question at Delphi changing Chars in string - missunderstood behavior - XE3 and is used from Remy's answer. The DecodeChar is, I believe simply unimportant here, or is it?
Also, the same goes with the function to save settings, which is called at FormClose event:
This is:
procedure TScribbles.SaveSettings;
var SettingsBuffToSave: TStringList;
begin
SettingsBuffToSave:=TStringList.Create;
Encoding := TEncoding.ANSI;
// Showmessage(settingsbuffer.Strings[0]);
SettingsBuffToSave:=Encode(SettingsBuffer);
// Showmessage(settingsbufftosave.Strings[0]);
SettingsBuffToSave.SaveToFile('bin/settings.txt', Encoding);
end;
With the first ShowMessage used as code instead of comment, it works, while otherwise in a comment function as it is written above, it calls external exception the same way as on Decode.
Is it possible, that the SettingsBuffToSave is just not yet created when it already calls the function Encode, or what?
At that time, the SettingsBuffer exists and is populated, so it really seems weird that it raises errors, which disappears with simply putting ShowMessage in there.
(Function Encode is basically a mirror of Decode, so the code is not important here...)

This code is VERY VERY VERY dangerous on many levels. Using objects across the DLL boundary in an unsafe manner. Mismanagement of object pointers across function calls. You need a redesign. Try the following as a start:
procedure Decode(Buff: PChar; BuffLen: Integer; ListIndex: Integer); stdcall; export;
var
u: integer;
begin
for u := 0 to BuffLen-1 do
begin
Buff^ := DecodeChar(Buff^, (ListIndex mod 5) + 1);
Inc(Buff);
end;
end;
procedure Encode(Buff: PChar; BuffLen: Integer; ListIndex: Integer); stdcall; export;
var
u: integer;
begin
for u := 0 to BuffLen-1 do
begin
Buff^ := EncodeChar(Buff^, (ListIndex mod 5) + 1);
Inc(Buff);
end;
end;
procedure Decode(Buff: PChar; BuffLen: Integer; ListIndex: Integer); stdcall; external '...';
procedure Encode(Buff: PChar; BuffLen: Integer; ListIndex: Integer); stdcall; external '...';
procedure LoadSettings;
var
h: Integer;
begin
SettingsBuffer := TStringList.Create;
SettingsBuffer.LoadFromFile('bin/settings.txt', TEncoding.ANSI);
for h := 0 to SettingsBuff.Count-1 do
begin
Decode(PChar(SettingsBuff[h]), Length(SettingsBuff[h]), h);
end;
end;
procedure TScribbles.SaveSettings;
var
h: Integer;
begin
for h := 0 to SettingsBuff.Count-1 do
begin
Encode(PChar(SettingsBuff[h]), Length(SettingsBuff[h]), h);
end;
SettingsBuff.SaveToFile('bin/setpb95enc.dll', TEncoding.ANSI);
end;

The obvious problem here is that the code exists in a DLL. Most likely you didn't arrange for the DLL to share its host's heap. And a Delphi class cannot be passed across a DLL boundary.
If you want to share Delphi classes between modules, you must use packages. Of course, another option is to put all the code in the same module. That is remove the DLL, and compile everything in the executable. The final option is to use valid interop types for DLLs.
Of course, there could be other reasons for the actual error. The code smells bad. For instance, what is this:
DecodeBuffer:=Buff;
Is DecodeBuffer a global variable? If so then it is plausible that you refer to the object after it has been destroyed. Not that I can see evidence of anything being destroyed. Without wishing to seem rude, your code looks like it may have multiple problems. As a matter of urgency you need to:
Deal with the DLL problem described above.
Remove global variables.
Fix lifetime issues. Stop leaking.
Enable range checking to locate buffer overruns.
Add FastMM in debug mode to try to catch heap corruptions.

I think I know what's going on here: I think your stack is getting smashed.
Furthermore, I rather suspect the actual cause is the Decode procedure using an uninitialized variable. Your ShowMessage statement (it would be the first one that matters if I'm right) changes what's on the stack and thus changes the uninitialized variable.
If I'm right this is going to have some heisenbug attributes--anything you do to find out what's going on will change the value of the uninitialized variable.
One thing to try: Declare a large local variable (the idea is to use up stack space) and make sure it's not discarded by the compiler. This will move things in memory and thus likely defuse the blowup. If it works it's pretty conclusive at to what's going on.

Related

Is there a way to get size of a procedure?

I'm using the following routine to patch functions in the RTL.
procedure PatchCode(const AddrProc: Pointer; const CodeSize: NativeUInt;
const Code: Pointer);
var
OldProtect: Cardinal;
begin
VirtualProtect(AddrProc, CodeSize, PAGE_EXECUTE_READWRITE, OldProtect);
Move(Code^, AddrProc^, CodeSize);
VirtualProtect(AddrProc, CodeSize, OldProtect, OldProtect);
end;
However when I tweak my patch-methods their size changes causing code like this to break:
//PatchRedirect calls PatchCode internally
PatchRedirect(AddrGetMem,{codesize = }17, #RedirectGetMem, JUMPS_GETMEM);
Is there a way to determine the size of a method at compile-time or runtime? (either one is fine).
I'm hoping for a general solution, but
if it only works for asm routines that's fine for my purposes.
Use case
One use case for this is a faster version of FillChar.
99% of the time FillChar is used as a ZeroMem.
So I patch System.ZeroMem with:
xor r8,r8
jmp FastFillChar;
and I patch System.FillChar with
movzx R8,R8b
mov r9,$0101010101010101
imul r8,r9
jmp FastFillChar
That way I can make the FillChar a tiny bit faster for those 99% of cases.
Or it would if anyone bothered to actually use zeromem
Update
Thanks to Rudy I think I have a solution applicable to a limited subset.
Is there a way to get size of a procedure?
If you have access to the source code, yes.
Delphi puts the generated code of routines in the same order as it is declared in the implementation section.
As long as the destination code you are trying to patch and the source code you're getting your patches from are compiled with the same parameters of {$CODEALIGN n} there is no problem.
For Win32, the default value is 4 and the Win32 RTL is compiled with alignment 4.
The code alignment for the Win64 RTL is {$CodeAlign 16}.
As long as the code alignment in your code and the patch recipient matches it the following code will work fine:
ProcSize:= NativeInt(#Routine2) - NativeInt(#Routine1);
PatchCode(#Routine1, ProcSize, #System.Something);
Any alignment nops will only increase the size up to the next multiple of $CodeAlign and the destination code is aligned the same way so you should be fine.
Obviously Routine1 had better be really short, otherwise you'll run into trouble, perhaps it's a good idea to assert that #dest is not a naked jmp to some other routine before patching if ProcSize > $CodeAlign.
I once wrote a piece of patching code myself, which doesn't overwrite the entire function at all, but just a jump to it at the start of the procedure. The size of the old procedure is of little importance because of that. Additionally, the class remembers the original content, so you can also 'unhook' the procedure by restoring that code.
It's written a long time ago, and I didn't have to use it in a long time, so I hope it still works in a more modern environment.
unit BigProcHook;
interface
uses
Windows, sysUtils;
type
PHack = ^THook;
THook = packed record
OpCodeCall : Byte;
OFFTo : Integer;
OpCodeRet : Byte;
end;
TBackup = THook;
TBigProcHook = class
private
FOldProc, FNewProc: Pointer;
FBackupped: Boolean;
FHooked: Boolean;
FOriginal: TBackup;
procedure SetHooked(const Value: Boolean);
protected
procedure InstallHook(Hook: THook);
procedure OverwriteProc;
public
constructor Create(AOldProc, ANewProc: Pointer; Install: Boolean = True);
property Hooked: Boolean read FHooked write SetHooked;
end;
implementation
{ TBigProcHook }
constructor TBigProcHook.Create(AOldProc, ANewProc: Pointer;
Install: Boolean);
begin
inherited Create;
FOldProc := AOldProc;
FNewProc := ANewProc;
if Install then
SetHooked(True);
end;
procedure TBigProcHook.InstallHook(Hook: THook);
var
OldProtect: Cardinal;
begin
// Change protection of oldproc memory
if VirtualProtect(FOldProc, SizeOf(THook), PAGE_EXECUTE_READWRITE, OldProtect) then
try
if not FBackupped then
begin
Move(FOldProc^, FOriginal, SizeOf(THook));
FBackupped := True;
end;
// Overwrite the old procedure
Move(Hook, FOldProc^, SizeOf(THook));
finally
VirtualProtect(FOldProc, SizeOf(THook), OldProtect, OldProtect);
end
else
begin
RaiseLastOSError;
end;
end;
procedure TBigProcHook.OverwriteProc;
// Overwrites the first few calls of OldProc with a call to NewProc and a Ret.
var
Hook: THook;
begin
// Create a tiny little redirection
with Hook do begin
OpCodeCall := $E8; // = CALL}
OFFTo := PAnsiChar(FNewProc) - PAnsiChar(FOldProc) - 5;
OpCodeRet := $C3; // = RET
end;
InstallHook(Hook);
end;
procedure TBigProcHook.SetHooked(const Value: Boolean);
begin
// Toggle hook.
if FHooked <> Value then
if Value then
OverwriteProc
else
InstallHook(FOriginal);
FHooked := Value;
end;
initialization
end.
Which you can call like this: (in the example it's called in the initialization and finalization of a unit)
var
FHook: TBigProcHook;
initialization
FHook := TBigProcHook.Create(#ProcedureToReplace, #ReplacementProcedure);
finalization
FHook.Hooked := False;
FHook.Free;
Originally posted on the Dutch forum NLDelphi.com.

How to call a nested function with AsyncCalls

I have this piece of working code using AsyncCalls 2.99 in the modified version by Zarko Gajic:
function TForm1.DoIt(i:integer):integer;
begin
end;
procedure TForm1.Main;
//-------------------------------------------------------
procedure CallIt;
begin
TAsyncCalls.Invoke(
procedure
var i:integer;
begin
For i := 0 to 10 do
If i < 11
then TAsyncCalls.Invoke<integer>(DoIt,i));
end);
end;
//-------------------------------------------------------
begin
CallIt;
end;
Now I would like to move the function DoIt into Main to be a nested function next to CallIt:
procedure TForm1.Main;
//-------------------------------------------------------
function DoIt(i:integer):integer;
begin
end;
//-------------------------------------------------------
procedure CallIt;
begin
TAsyncCalls.Invoke(
procedure
var i:integer;
begin
For i := 0 to 10 do
If i < 11
then TAsyncCalls.Invoke<integer>(DoIt,i));
end);
end;
//-------------------------------------------------------
begin
CallIt;
end;
The above code (naturally) does not work. As much as I unterstand Invoke requires a method as parameter and a nested function isn't one.
Invoke expects a TAsyncCallArgGenericMethod:
class function Invoke<T>(Event: TAsyncCallArgGenericMethod<T>; const Arg: T): IAsyncCall; overload; static;
TAsyncCallArgGenericMethod<T> = function(Arg: T): Integer of object;
I have already received a hint to convert the TAsyncCallArgGenericMethod into a reference:
TAsyncCallArgGenericMethod<T> = reference to function(Arg: T): Integer;
Although I have the general notion (i.e. illusion) that I understand the concept I have not been able to produce working code.
Now I would like to move the function DoIt into Main to be a nested function next to CallIt:
You can not call nested function from outside the function containing it - because nested functions need to access the outer(containing) function local variables, that only exists while executing code inside that containing function.
Even if the particular nested function does not evaluate their rights of accessing those local variables - it has those rights and the compiler should be able to produce all the lo-level scaffolding for that.
Specifically in your snippet, You can not call TForm1.Main.DoIt from outside of the TForm1.Main itself. So you can not take the reference to it and pass it to some external body like AsyncCall dispatcher.
It does not depend upon whether you would use procedure of object or reference to procedure or any other type - it is the fundamental property of nested function that they "exist" only locally to the containing function and only can be run when the outer function runs. AsyncCall would most probably try to run the function when TForm1.Main would be exited and thus its local variables stack frame required by TForm1.Main.DoIt would not exist.
You have to find some other way to "pack" those functions together, nested functions would not do here.
For example one may try using Advanced Records here.
Try to arrange it somehow like that:
type
TForm1 = class(TForm)
....
private
type Dummy = record
procedure CallIt;
procedure DoIt(const i:integer);
end;
end;
....
//-------------------------------------------------------
procedure TForm1.Dummy.CallIt;
begin
TAsyncCalls.Invoke(
procedure
var i:integer;
begin
For i := 0 to 10 do
If i < 11
then TAsyncCalls.Invoke<integer>(DoIt,i));
end);
end;
procedure TForm1.Dummy.DoIt(const i:integer);
begin
end;
procedure TForm1.Main;
var d: Dummy;
begin
d.CallIt;
end;
Also, I think your approach is wrong here: you would instantly form many-many threads exhausting your OS resources.
I would suggest you using OmniThreadLibrary instead, where there are hi-level Parallel-Loop and Collection-Pipeline concepts. They would give you benefit of automatic threads pool management, so you would only have so many worker threads as your CPU can bear, adapting your program to any hardware it would happen to run on.
I may also have the illusion that I understand these things (i.e. I may be wrong) so take this with a pinch of salt, but this is my take on it.
A nested function has access to all parameters available to the calling function (including self), but has no 'hidden' parameters (it doesn't need any). The class function on the other hand has a hidden parameter (called 'self') that the function accesses to find the object that is actually calling the function. Thus the signatures are totally different.
If you go back to the olden days when C++ was an interpreter, something like Fred.Main( x, y) in C++ would be translated to something like Main( Fred, x, y) in C. I only include this to illustrate how that hidden parameter works.
So the upshot is you can't do what you are trying to do because by moving DoIt inside your Main function, you are completely changing its signature, and indeed how it works.
I just couldn't leave it at that since for some reason I really had sunk my teeth into it. Now, here's a solution. Not a solution I would recommend, but a solution.
There has been a discussion here on stackoverflow some 4 years ago. David quoted the documentation and continued:
If I recall correctly, an extra, hidden, parameter is passed to nested functions with the pointer to the enclosing stack frame. This is omitted in 32 bit code if no reference is made to the enclosing environment.
Sertaç Akyüz apparently poked around in the assembler code and reported:
It's an implicit parameter alright! The compiler assumes it has its thing in 'rcx' and the parameters to the function are at 'rdx' and 'r8', while in fact there's no 'its thing' and the parameters are at 'rcx' and 'rdx'.
This seemed to finish the whole thing.
But then, there is this text: How to pass a nested routine as a procedural parameter (32 bit). A rather surprising title if you consider the documentation. This led to the following code:
{unit AsyncCalls;}
TAsyncCalls = class(TObject)
private
type
…
//TAsyncCallArgGenericMethod<T> = function(Arg: T): Integer of object;
TAsyncCallArgGenericMethod<T> = reference to function(Arg: T): Integer;
uses … ,AsyncCalls,AsyncCallsHelper;
procedure TForm1.Main;
//-------------------------------------------------------
function DoIt(i:integer):integer;
begin
Result := i;
end;
//-------------------------------------------------------
procedure CallIt;
var p:Pointer;
begin
p := #DoIt;
TAsyncCalls.Invoke(
procedure
var i:integer;
begin
For i := 0 to 10 do
If i < 11 then
AsyncHelper.AddTask(TAsyncCalls.Invoke<integer>(p,i));
end);
end;
//-------------------------------------------------------
begin
CallIt;
end;
This code works. As I mentioned before, I wouldn't recommend using it, but it works. I learned a lot in the course of finding a solution which I now consider the main benefit.

SetWindowsHookEx freeze my keyboard and mouse, and other strange things happends

I'm writing joke program (makes strange sounds, where user use mouse or keyboard) in Delphi, which uses global hooks to capture mouse and keyboard events.
This is function responsible for handle this hook:
procedure MKHOOK(code: Integer;wp: wParam;lp: lParam); stdcall;
var
hh: HHOOK;
begin
PlaySound('fart.wav');
CallNextHookEx(hh,code,wp,lp);
end;
Hook starts, when Form is creating:
procedure TForm6.FormCreate(Sender: TObject);
begin
MH := SetWindowsHookEx(WH_MOUSE_LL,#MKHOOK,hInstance,0);
KH := SetWindowsHookEx(WH_KEYBOARD_LL,#MKHOOK,hInstance,0);
end;
Where MH and KH are type of HHOOK
Hook is deleted, when program ends:
procedure TForm6.FormDestroy(Sender: TObject);
begin
UnhookWindowsHookEx(MH);
UnhookWindowsHookEx(KH);
end;
Problems starts, when I try to run it there are strange things happends like:
Mouse and keyboard stopped working, I must use ctrl + alt + del to recover it
Some keys didn't working after program execution, when I stoped it (ctrl, alt, tab)
Some keys change their behavior for example, when I used arrows keys, they turn my screen view.
What is wrong with this code? Why it not works? Sorry for my English ;)
There are a few things wrong here. The most obvious:
The call to PlaySound is too expensive to make in a hook. It's liable to bring the system to its knees if you call PlaySound every time your hook executes.
Your hook signatures are wrong. They need to be functions that return LRESULT. The required return value is described in the documentation in both cases as follows: if nCode is less than zero, the hook procedure must return the value returned by CallNextHookEx.
You don't do any error checking. Always check API calls for errors. Error handling is discussed in the documentation for each API call.
The first parameter of CallNextHookEx is ignored so you may as well pass 0. And it is cleaner to use separate hook procedures in my view.
The hook functions have to look like this:
function MouseHook(code: Integer; wp: wParam; lp: lParam): LRESULT; stdcall;
begin
Result := CallNextHookEx(0, code, wp, lp);
end;
function KeyboardHook(code: Integer; wp: wParam; lp: lParam): LRESULT; stdcall;
begin
Result := CallNextHookEx(0, code, wp, lp);
end;
Obviously these hooks don't do anything yet, but let's walk before we try to run.
Install the hooks like this:
MH := SetWindowsHookEx(WH_MOUSE_LL, #MouseHook, hInstance, 0);
if MH = 0 then
// handle error
KH := SetWindowsHookEx(WH_KEYBOARD_LL, #KeyboardHook, hInstance, 0);
if KH = 0 then
// handle error
I think it's clear that you have not read the documentation in sufficient detail. These APIs are tricky to use. You need precision in your coding. I recommend that you read the documentation again more closely.

Iterate over variables in Delphi

Given the declarations :
Unit MyUnit;
interface
type
TMyFileStream= class(TFileStream);
...
end;
var
a1,a2,a3,a4,a5: integer;
b1,b2,b3: boolean;
c1: char;
d1,d2,d3,d4: TDateTime;
f1,f2,f3,f4,f5,f6,f7,f8: TMyFileStream // LineX
...
procedure MyProc;
implementation
procedure MyProc
begin
// I wanna iterate over all integer (or any other type) variables here with a loop regardless of their count and identifier name
end;
Some specific type variables' count regularly changes in code - mostly increases as I add new functions. How can I reference them in a loop to take the same action on all of them ? I want to preserve the fact that when I add a new one, the code needs to be modified at only one place.
I've already thought of putting them in an (either static or dynamic) array, but this involves the modifocation of code at every location where they are referenced, which is much-much-much work that I wanna spare if it's possible by any means.
There's currently 38 variables I want to take an acton upon, the references' count is a multiple of it far above 100.
Hope I was clear enough.
Thanks for any idea.
Peter
Although the design smells, this is what pointers are made for:
type
PMyFileStream = ^TMyFileStream;
TMyFileStream= class(TFileStream)
end;
var
a1,a2,a3,a4,a5: integer;
b1,b2,b3: boolean;
c1: char;
d1,d2,d3,d4: TDateTime;
f1,f2,f3,f4,f5,f6,f7,f8: TMyFileStream; // LineX
function GetVarsInt: TArray<PInteger>;
begin
result := TArray<PInteger>.Create(#a1, #a2, #a3, #a4, #a5);
end;
function GetVarsBool: TArray<PBoolean>;
begin
result := TArray<PBoolean>.Create(#b1, #b2, #b3);
end;
function GetVarsChar: TArray<PChar>;
begin
result := TArray<PChar>.Create(#c1);
end;
function GetVarsDateTime: TArray<PDateTime>;
begin
result := TArray<PDateTime>.Create(#d1, #d2, #d3, #d4);
end;
function GetVarsMyFileStream: TArray<PMyFileStream>;
begin
result := TArray<PMyFileStream>.Create(#f1, #f2, #f3, #f4, #f5, #f6, #f7, #f8);
end;
procedure HandleInt(var Value: Integer);
begin
end;
procedure HandleBool(var Value: Boolean);
begin
end;
procedure HandleChar(var Value: Char);
begin
end;
procedure HandleDateTime(var Value: TDateTime);
begin
end;
procedure HandleMyFileStream(var Value: TMyFileStream);
begin
end;
procedure MyProc;
var
vInt: PInteger;
vBool: PBoolean;
vChar: PChar;
vDateTime: PDateTime;
vMyFileStream: PMyFileStream;
begin
for vInt in GetVarsInt do
HandleInt(vInt^);
for vBool in GetVarsBool do
HandleBool(vBool^);
for vChar in GetVarsChar do
HandleChar(vChar^);
for vDateTime in GetVarsDateTime do
HandleDateTime(vDateTime^);
for vMyFileStream in GetVarsMyFileStream do
HandleMyFileStream(vMyFileStream^);
end;
In case of the TMyFileStream variables, you might get away with no pointers when you only want to manipulate the existing object instances.
If you put these variables in a class you can use RTTI to loop over the properties of that class. There is no method that I know of to loop over variables that do not belong to a class.
I've already thought of putting them in an (either static or dynamic) array, but this involves the modifocation of code at every location where they are referenced, which is much-much-much work that I wanna spare if it's possible by any means.
So what! Do it!
The longer you put off fixing horrible code, the worse it will get. Also, it's not nearly as bad as you think.
E.g. Change the following:
var
a1,a2,a3,a4,a5: integer;
to:
var
A: array[1..5] of Integer;
Now everything that was referring to a? will fail to compile (unless you had scope conflicts, which would be a simmering pot of disaster in any case). These compilation errors can easily be fixed by changing a? to a[?].
If you simply go through a cycle of: compile --> fix --> compile --> fix --> ... You'll find you can clean up a lot faster than you think.

Need to resolve HMONITOR --> deviceName (or deviceName --> HMONITOR) in windows

EDIT - See Update at end
This is for Delphi 7.0 Build 4.453
Summary
I need to be able to take the Handle property from a TMonitor object (an element in the Monitors array in the TScreen component) which is a HMONITOR, and turn it into the string you would use in calls to EnumDisplaySettings as the lpszDeviceName parameter.
(my end goal is to get a list of device settings from a given HMONITOR value, by passing the resolved lpszDeviceName into calls to EnumDisplaySettings).
Detailed Information
As mentioned above, the Screen.Monitors[x].Handle property is of type HMONITOR and is normally used to pass into the GetMonitorInfo function, which returns, geometry information, but no lpszDeviceName. (note: there is a TMonitorInfoEx structure that has a szDevice field, but it does not seem to get filled in on my system, even though i am setting the cbSize field to the appropriate size).
Alternatively, if i can use a szDeviceName to get the equivalent HMONITOR value, i could plug it into the following function, which would use it in a comparison (I have inserted a call to fictitious function called hMonitorFromDeviceName in the code below) to indicate how it would be used.
function GetMonitorDeviceName(hmon : HMONITOR) : string;
var
DispDev : TDisplayDevice;
deviceName : string;
nDeviceIndex : integer;
begin
Result := '';
FillChar(DispDev, sizeof(DispDev),0);
DispDev.cb := sizeof(DispDev);
nDeviceIndex := 0;
while (EnumDisplayDevices(nil, nDeviceIndex, DispDev, 0)) do
begin
if ( hMonitorFromDeviceName(DispDev.DeviceString) = hmon ) then
begin
Result := StrPas(DispDev.DeviceString);
exit;
end;
inc(nDeviceIndex);
end;
end;
Update
Thanks to David Heffernan, I have tested his solution, and here is a sample function to get the monitor name from a given handle:
function GetMonitorName(hmon : HMONITOR) : string;
type
TMonitorInfoEx = record
cbSize: DWORD;
rcMonitor: TRect;
rcWork: TRect;
dwFlags: DWORD;
szDevice: array[0..CCHDEVICENAME - 1] of AnsiChar;
end;
var
DispDev : TDisplayDevice;
deviceName : string;
monInfo : TMonitorInfoEx;
begin
Result := '';
monInfo.cbSize := sizeof(monInfo);
if GetMonitorInfo(hmon,#monInfo) then
begin
DispDev.cb := sizeof(DispDev);
EnumDisplayDevices(#monInfo.szDevice, 0, DispDev, 0);
Result := StrPas(DispDev.DeviceString);
end;
end;
I think that you must be calling GetMonitorInfo incorrectly. This code:
{$APPTYPE CONSOLE}
uses
SysUtils, MultiMon, Windows, Forms;
var
i: Integer;
MonitorInfo: TMonitorInfoEx;
begin
MonitorInfo.cbSize := SizeOf(MonitorInfo);
for i := 0 to Screen.MonitorCount-1 do
begin
if not GetMonitorInfo(Screen.Monitors[i].Handle, #MonitorInfo) then
RaiseLastOSError;
Writeln(MonitorInfo.szDevice);
end;
Readln;
end.
produces this output on my machine:
\\.\DISPLAY1
\\.\DISPLAY2
I suspect that your call to GetMonitorInfo is failing in some way and perhaps you are not checking the return value for errors.
Having searched QualityCentral I suspect you have fallen victim to a known bug in older versions of Delphi: QC#3239. This is reported fixed in version 10.0.2124.6661 which is Delphi 2006.
Your comments confirm this diagnosis. To fix the problem you'll need a new TMonitorInfoEx definition. Here's one that will work on your pre-Unicode Delphi:
type
TMonitorInfoEx = record
cbSize: DWORD;
rcMonitor: TRect;
rcWork: TRect;
dwFlags: DWORD;
szDevice: array[0..CCHDEVICENAME - 1] of AnsiChar;
end;
If you add that to the code above (before you declare the variables of course) then I believe it will resolve your problem.
As an interesting aside, even in XE3, these structs have not been translated correctly: QC#114460. Admittedly the error is rather benign as it only affects PMonitorInfoExA and TMonitorInfoExA, but the error caught me out whilst trying to solve the problem in this question!

Resources