I had always wanted to donate to those projects but never came around actually doing that. So, having some ready to use credits on PayPal made me finally do it.
DelphiComments Off on Showing graphics in a Delphi StringGrid and switching them by mouse or keyboard
Apr162017
Today I had a need for a grid that looks like this:
It mixes text with two different bitmaps, representing an on/off state. In addition it should allow the user to switch the states using the mouse and the keyboard.
(If you are an experienced component developer, expect to be bored by this article. đ )
I started out with a TListView in report view but when it came to switching with the keyboard I realized that I could only select the first column (or the whole row). I’m a fan of using the keyboard, so this was a no no. So I replaced the TListView with a TStringGrid. Adding a few events got me what I wanted:
OnDrawCell for drawing the bitmaps
OnMouseDown for intercepting mouse clicks
OnKeyPress for intercepting keyboard presses
I used a TImageList with two images for the bitmaps and set the cells that represent an on/off state to ‘0’ and ‘1’ (I could have used ‘Y’ and ‘N’ but ‘0’ and ‘1’ nicely correspond to the indices in the ImageList.)
I won’t go into this preliminary solution too much because the next step was to make this easier to use than having to set up the events each time I want to use it.
So, what else can we do? Create a component that descends from TStringGrid of course. But I didn’t want to install it, so I made it an interposer class instead.
The code above declares a new type TStringGrid which descends from a class with the same name declared in unit Grids. It adds a field and a few methods.
As you can see, there isn’t anything special here. We just set the cells to strings.
Now to the more interesting code.
The string grid needs to know where to get its images from. Since I want to use a simple interposer class that should not require any special initialization code in the form, I decided it should get its image list from its owner in the method Loaded, it assumes that it is called ‘TheImageList’.
procedure TStringGrid.Loaded;
var
cmp: TComponent;
begin
inherited;
cmp := Owner.FindComponent('TheImageList');
if Assigned(cmp) and (cmp is TImageList) then
FImageList := TImageList(cmp);
end;
The method Loaded is called by the streaming system after all components of a form have been created and initialized, so it is safe to assume that the image list exists.
Now, that the component has been set up, here is the code for drawing the bitmaps.
procedure TStringGrid.DrawCell(ACol, ARow: Integer; ARect: TRect;
AState: TGridDrawState);
var
s: string;
bmp: TBitmap;
xOff: Integer;
YOff: Integer;
begin
inherited;
if not Assigned(FImageList) or (ARow = 0) or (ACol in [0, 5]) then
Exit;
s := Cells[ACol, ARow];
Canvas.FillRect(ARect);
bmp := TBitmap.Create;
try
if (s = '1') then
FImageList.GetBitmap(1, bmp)
else
FImageList.GetBitmap(0, bmp);
xOff := ARect.Left + ((ARect.Right - ARect.Left) - bmp.Width) div 2;
YOff := ARect.Top + ((ARect.Bottom - ARect.Top) - bmp.Height) div 2;
Canvas.Draw(xOff, YOff, bmp);
finally
FreeAndNil(bmp);
end;
end;
Originally I had put this code into the OnDrawCell event.
It first checks that the image list is assigned and that the cell to draw is one for which I want to draw an image. It then reads the string from the cell and uses it to determine the bitmap to draw. It could have gone a step further and convert the string to an integer and use that as an index into the image list to select from more than two images. It then gets the image as a bitmap and draws it centered on the cell.
That takes care of painting the grid. The rest is even easier: We want to switch between ‘0’ and ‘1’ using the mouse and the keyboard.
procedure TStringGrid.KeyPress(var Key: Char);
begin
inherited;
if Key <> ' ' then
Exit;
SwitchCell(Col, Row);
end;
For the keyboard, we use the KeyPress method (originally assigned to the OnKeyPress event), check for the space key and call SwitchCell for the current column and row.
For the mouse, we first need to get the cell on which the user clicked. That’s simple because TStringGrid has a method for it: MouseToCell. We then pass column and row to SwitchCell as above.
procedure TStringGrid.MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer);
var
C: Integer;
r: Integer;
begin
inherited;
MouseToCell(X, Y, C, r);
SwitchCell(C, r);
end;
And finally the SwitchCell method. It checks again whether the cell is one we actually want to switch and then changes the cell’s string from ‘1’ to ‘0’ and vice versa.
procedure TStringGrid.SwitchCell(_Col, _Row: Integer);
var
s: string;
begin
if (_Row = 0) or (_Col in [0, 5]) then
Exit;
s := Cells[_Col, _Row];
if s = '1' then
s := '0'
else
s := '1';
Cells[_Col, _Row] := s;
end;
Pretty easy, wasn’t it? So, why did I write this blog post?
Being a lazy bastard™ I of course googled for a solution first. I didn’t find any ready made examples for what I wanted to do so I thought I’d post one for others who are as lazy as I am. And maybe I will need it myself at a later time, so I can google it again.
If you want to say thank you for my work on GExperts, there are the following options:
Contribute to the GExperts quality
You can either contribute good bug reports or – even better – bug fixes to GExperts. I’d actually prefer that over getting money. Good bug reports include the Delphi version (and language), the Windows version, the GExperts version, the settings used and steps to reliably reproduce the problem. Screen shots might also help. Use the bug report form on Sourceforge for that.
You can do even more by taking over the maintenance of GExperts for a particular Delphi version. I am currently using Delphi 2007 for most of my work, with a bit of Delphi XE2 and the latest version sprinkled in. So most of the other versions don’t really get tested. If you regularly use e.g. Delphi 6, 7, 2005, 2006, 2009 or 2010, why not take over testing and possibly bug fixing GExperts for these versions? For now, I still have working installations of these Delphi versions, but I am not sure that I will go to much effort to keep them working in the future. If one of them breaks, I might decide to drop GExperts support for them. By taking on the responsibility for them, you ensure they will still be around in a few years.
Contribute new features to GExperts
Contributing features to GExperts involves more than just suggestions (even though I am open to these too, please use the feature requests form page on SourceForge for that.)
It’s also not only about implementing them and sending me patches. It’s about thinking about how to integrate them into the existing code and UI. If you want to contribute code, please feel free to contact me.
Donate money
If you can’t do any of the above, you can donate money. I suggest the following amounts:
50 Euros per year for private use via Flattr or
100 Euros per developer per year for commercial use via Flattr or
It’s up to you of course. If you think it’s worth more, I definitely won’t complain, but if you can’t afford these, smaller amounts are also OK.
Note: I deleted my Flattr account on 2017-10-21 due to Flatter changing their terms of service.
Delphi, dzLibComments Off on Bugfix for the Build Connection String dialog appearing on the wrong monitor
Apr142017
According to MSDN the window handle passed to IDBPromptInitialize::PromptDataSource will be used this way:
hWndParent [in]
The parent window handle for dialog boxes to be displayed. The dialog box will always be centred within this window.
Unfortunately it doesn’t work like this: If the parent form is on a secondary monitor the dialog is shown on the primary monitor on the side adjacent to the secondary monitor. It works fine when the parent form is on the primary monitor.
This is the code I used:
class function TConnectionInfoRec.EditConnectionString(_ParentHandle: HWND;
var _ConnectionString: string): Boolean;
var
DataInit: IDataInitialize;
DBPrompt: IDBPromptInitialize;
DataSource: IUnknown;
InitStr: PWideChar;
s: WideString;
begin
DataInit := CreateComObject(CLSID_DataLinks) as IDataInitialize;
if _ConnectionString <> '' then begin
s := _ConnectionString;
DataInit.GetDataSource(nil, CLSCTX_INPROC_SERVER,
PWideChar(s), IUnknown, DataSource);
end;
DBPrompt := CreateComObject(CLSID_DataLinks) as IDBPromptInitialize;
Result := Succeeded(DBPrompt.PromptDataSource(nil, _ParentHandle,
DBPROMPTOPTIONS_PROPERTYSHEET, 0, nil, nil, IUnknown, DataSource));
if Result then begin
InitStr := nil;
DataInit.GetInitializationString(DataSource, True, InitStr);
_ConnectionString := InitStr;
end;
end;
Called as
procedure TForm1.b_TestClick(Sender: TObject);
var
s: string;
begin
if TConnectionInfoRec.EditConnectionString(Self.Handle, s) then
ed_Result.Text := s;
end;
I asked on Google+ for help but nobody could find an error in my code, so it apparently is a bug in Windows.
So I set out to find a workaround: Start a background thread (since the foreground thread is blocked by showing that dialog) that finds the dialog and moves it to the correct place.
Unfortunately GetActiveWindow didn’t return this window, so I resorted to GetForegroundWindow and checked the window’s process id with GetThreadProcessId to make sure I got the correct window. While this worked, it left a bad smell. Using EnumThreadWindow as suggested by Attila Kovacs also worked, so I used that instead.
But while writing this blog post, I reread the GetActiveWindow documentation and noticed the reference to GetGUIThreadInfo which sounded as if it might do exactly what I was looking for. And lo and behold, it actually does. So here is the solution I eventually used:
type
TMoveWindowThread = class(TNamedThread)
private
FParentHandle: HWND;
FParentCenterX: Integer;
FParentCenterY: Integer;
procedure CenterWindow(wHandle: hwnd);
protected
procedure Execute; override;
public
constructor Create(_ParentHandle: HWND);
end;
{ TMoveWindowThread }
constructor TMoveWindowThread.Create(_ParentHandle: HWND);
begin
FreeOnTerminate := True;
FParentHandle := _ParentHandle;
inherited Create(False);
end;
procedure TMoveWindowThread.CenterWindow(wHandle: hwnd);
var
Rect: TRect;
WindowCenterX: Integer;
WindowCenterY: Integer;
MoveByX: Integer;
MoveByY: Integer;
begin
GetWindowRect(wHandle, Rect);
WindowCenterX := Round(Rect.Left / 2 + Rect.Right / 2);
WindowCenterY := Round(Rect.Top / 2 + Rect.Bottom / 2);
MoveByX := WindowCenterX - FParentCenterX;
MoveByY := WindowCenterY - FParentCenterY;
MoveWindow(wHandle, Rect.Left - MoveByX, Rect.Top - MoveByY,
Rect.Right - Rect.Left, Rect.Bottom - Rect.Top, False);
end;
procedure TMoveWindowThread.Execute;
var
Rect: TRect;
MaxTickCount: DWORD;
ThreadInfo: TGUIThreadinfo;
begin
inherited;
GetWindowRect(FParentHandle, Rect);
FParentCenterX := Round(Rect.Left / 2 + Rect.Right / 2);
FParentCenterY := Round(Rect.Top / 2 + Rect.Bottom / 2);
ThreadInfo.cbSize := SizeOf(ThreadInfo);
MaxTickCount := GetTickCount + 10000; // 10 Seconds should be plenty
while MaxTickCount > GetTickCount do begin
Sleep(50);
if GetGUIThreadInfo(MainThreadID, ThreadInfo) then begin
if ThreadInfo.hwndActive <> FParentHandle then begin
CenterWindow(ThreadInfo.hwndActive);
Exit;
end;
end;
end;
end;
This thread is started from EditConnectionString by inserting the following code:
// ...
DBPrompt := CreateComObject(CLSID_DataLinks) as IDBPromptInitialize;
if _ParentHandle <> 0 then begin
// This is a hack to make the dialog appear centered on the parent window
// According to https://msdn.microsoft.com/en-us/library/ms725392(v=vs.85).aspx
// the dialog should automatically be centered on the passed parent handle,
// but if the parent window is not on the primary monitor this does not work.
// So, we start a background thread that waits for the dialog to appear and then
// moves it to the correct position.
TMoveWindowThread.Create(_ParentHandle);
end;
Result := Succeeded(DBPrompt.PromptDataSource(nil, _ParentHandle,
DBPROMPTOPTIONS_PROPERTYSHEET, 0, nil, nil, IUnknown, DataSource));
// ...
Unfortunately the dialog briefly still appears in the wrong position. I found now way around that.
DelphiComments Off on tdbf Packages for Delphi 10.2
Apr142017
Since we use it at work, I have had write access to the tdbf repository on SourceForge for a while. And apparently I am the only one who cares about support for the latest Delphi versions.
A while ago, after my post on Known IDE Packages in Delphi I wrote a the KnownIdePackagesManager tool which lists those packages, allows to disable and enable some of them and also set the package description for those packages that don’t have a meaningful description (usually “(Untitled)”).
ccpack, DelphiComments Off on Delphi Custom Container Pack updated for Delphi 10.2
Apr142017
I just updated the Custom Container Pack sources to support Delphi 10.2. It was mostly a matter of creating the packages for the “new” version. I also used the latest version of the Delphiversions.inc file.
It now compiles and installs. I have not tested it extensively.