How to set a procedure (function) to an event in delphi 7? - delphi-7

I am writing which component will run through the array like ['form1.Button1', 'form1.memo1', 'form1'] - like
And put the showms2 handler on it (form1.Button1.onclick: = showms2)
var
comp: array of Tcomponent;
met: Tmethod;
start off
setlength (comp, Lines.Count);
for i: = 0 to Lines.Count-1 do
start off
comp [i]: = (FindComponent (Lines.Names [i]) as TControl);
met: = GetMethodProp (comp [i], 'OnClick');
meth.Code: = form1.MethodAddress ('showms2');
met.Data: = 0;
// When splitting into elements, nothing happens, is there an alternative?

FindComponent() does not work the way you are using it. You need to specify only the name of a component that is directly owned by the component being searched, you can't specify a chain of components, ie Form1.FindComponent('Form1.Button1') won't ever work, but Form1.FindComponent('Button1') will work, if Form1 owns Button1.
Also, if you are going to set both TMethod.Code and TMethod.Data then calling GetMethodProp() is completely redundant and should be removed.
Also, you need to use SetMethodProp() to actually assign the TMethod to the target event.
Try this instead:
var
comp: TComponent;
met: TMethod;
i: Integer;
begin
for i := 0 to Lines.Count-1 do
begin
comp := FindComponent(Lines.Names[i]);
if comp <> nil then
begin
if IsPublishedProp(comp, 'OnClick') then
begin
meth.Code := Form1.MethodAddress('showms2');
meth.Data := Form1;
SetMethodProp(comp, 'OnClick', met);
end;
end;
end;
end;

Related

Setting `InitialDir` property of `TSelectDirectoryDialog` mutiple times

I'm trying to use the InitialDir property of TSelectDirectoryDialog:
procedure selectfolder;
begin
SelectDirectoryDialog1.InitialDir := strPath;
If SelectDirectoryDialog1.Execute then begin
Edit1.Text := SelectDirectoryDialog1.FileName;
end;
end;
The first time (with strPath=X) it works fine, the second time I'm using this procedure (with strPath=Y) it doesn't use the new path (Y), but the one I selected previously.
Do I have to call a method, something like SelectDirectoryDialog1."reinitiate" before I set the InitialDir property a second time? Another idea would be to use a different property then InitialDir, but I don't know which one would do the job. Unfortunately the doc page for TSelectDirectoryDialog is currently down, so I don't have a description for the available methods/properties for TSelectDirectoryDialog and the ones I tested to solve my problem.
I got it to work if I create the TSelectDirectoryDialog class instance manually and don't use the one from the Component Palette to create it "on the form". Then I just destroy the instance and create a new one.
procedure TForm1.Button4Click(Sender: TObject);
var SelectDirectoryDialogManual : TSelectDirectoryDialog;
begin
SelectDirectoryDialogManual := TSelectDirectoryDialog.Create(nil);
SelectDirectoryDialogManual.InitialDir := 'C:\Windows';
if SelectDirectoryDialogManual.Execute then ShowMessage(SelectDirectoryDialogManual.FileName);
SelectDirectoryDialogManual.Free;
end;
But how do I do that when I created SelectDirectoryDialog1 using the component Component Palette?
By saving and restoring the value of InitialDir before each invocation of Execute, or doing what #Sertac says in a comment, which works but is less "self-documenting" imo, ymmv.
The code below works fine for me. edInitialDir is a TEdit which saves the most recent directory selected using SelectDirectoryDialog1, which is then used for the next invocation.
procedure TForm1.Button1Click(Sender: TObject);
begin
SelectDirectoryDialog1.InitialDir := edInitialDir.Text;
if SelectDirectoryDialog1.Execute then
Caption := 'executed'
else
Caption := 'not executed';
edInitialDir.Text := SelectDirectoryDialog1.FileName;
end;
Note: All properties of SelectDirectoryDialog1 are the defaults for an instance freshly added from the Component Palette.
Regarding your comment, TSelectDirectoryDialog.Execute calls TWin32WSSelectDirectoryDialog.CreateHandle (see Dialogs.Pas, line 1219). The initial part of this is as follows:
class function TWin32WSSelectDirectoryDialog.CreateHandle(const ACommonDialog: TCommonDialog): THandle;
var
Options : TOpenOptions;
InitialDir : string;
Buffer : PChar;
bi : TBrowseInfo;
iidl : PItemIDList;
biw : TBROWSEINFOW;
Bufferw : PWideChar absolute Buffer;
InitialDirW: widestring;
Title: widestring;
DirName: string;
begin
DirName := '';
InitialDir := TSelectDirectoryDialog(ACommonDialog).FileName;
Options := TSelectDirectoryDialog(ACommonDialog).Options;
if length(InitialDir)=0 then
InitialDir := TSelectDirectoryDialog(ACommonDialog).InitialDir;
if length(InitialDir)>0 then begin
// remove the \ at the end.
if Copy(InitialDir,length(InitialDir),1)=PathDelim then
InitialDir := copy(InitialDir,1, length(InitialDir)-1);
// if it is a rootdirectory, then the InitialDir must have a \ at the end.
if Copy(InitialDir,length(InitialDir),1)=DriveDelim then
InitialDir := InitialDir + PathDelim;
end;
From this you can see that it initially attempts to derive the value of InitialDir from the FileName property and only if that results in an empty string does it attempt to use the stored value of the InitialDir property. This is why the dialog uses the previously-selected directory the next time Execute is invoked, which is exactly what you should be expecting, even if you do not like it. The only way to re-use the initial value of IntialDir from second and subsequent invocations is to restore it before each one.

Strange behavior with TThread.CreateAnonymousThread

I was unable to follow how it is working.
A very simple example first, to try explain my situation better.
This code is inside a new Form Form1 create in a new project. Where mmo1 is a Memo component.
TOb = class
Name : String;
constructor Create(Name : String);
procedure Go();
end;
procedure TOb.Go;
begin
Form1.mmo1.Lines.Add(Name);
end;
Then I have a button with this event:
procedure TForm1.btn4Click(Sender: TObject);
var
Index : Integer;
begin
mmo1.Lines.Clear;
for Index := 1 to 3 do
TThread.CreateAnonymousThread(TOb.Create('Thread ' + IntToStr(Index)).Go).Start;
end;
And my output on the memo is:
Thread 4
Thread 4
Thread 4
I really don't got it.
First question: Why the "Name" output is: Thread 4? Is a For loop from 1 to 3. At least should be 1 or 3
Second: Why it only execute the last thread "Thread 4", instead of 3 times in sequence "Thread 1", "Thread 2", "Thread 3"?
Why I'm asking this? I have an object that has already a process working fine. But now I found me in a situation that I need a List of this object to be processed. Sure work fine process one by one, but in my case they are independent one of other so I thought "hm, lets put them in threads, so it will run faster".
To avoid modifying the object to extend TThread and overriding Execute I look up on how to execute a thread with a procedure instead of an object that inherits from TThread and found the Anonymous Thread. Works really great with one object, but when I tried loop through my object list, strange behaviors happens.
This has the same effect.
for Index := 1 to 3 do
TThread.CreateAnonymousThread(
procedure
var
Ob : TOb;
begin
OB := TOb.Create('Thread ' + IntToStr(Index));
OB.Go;
end
).Start;
Sure I'm not clean the object, this was just some tests that I was running.
Any Ideas? Or in this case I will need to inherits from TThread and override the Execute methode?
The funny thing is that THIS runs just fine.
mmo1.Lines.Clear;
TThread.CreateAnonymousThread(TOb.Create('Thread ' + IntToStr(1)).Go).Start;
TThread.CreateAnonymousThread(TOb.Create('Thread ' + IntToStr(2)).Go).Start;
TThread.CreateAnonymousThread(TOb.Create('Thread ' + IntToStr(3)).Go).Start;
Output:
Thread 1
Thread 2
Thread 3
Works really great with one object, but when I tried loop through my object list, strange behaviors happens.
You are likely not taking into account how anonymous procedures bind to variables. In particular:
Note that variable capture captures variables--not values. If a variable's value changes after being captured by constructing an anonymous method, the value of the variable the anonymous method captured changes too, because they are the same variable with the same storage. Captured variables are stored on the heap, not the stack.
For example, if you do something like this:
var
Index: Integer;
begin
for Index := 0 to ObjList.Count-1 do
TThread.CreateAnonymousThread(TOb(ObjList[Index]).Go).Start;
end;
You will actually cause an EListError exception in the threads (I least when I tested it - I don't know why it happens. Verified by assigning an OnTerminate handler to the threads before calling Start(), and then having that handler check the TThread(Sender).FatalException property).
If you do this instead:
var
Index: Integer;
Ob: TOb;
begin
for Index := 0 to ObjList.Count-1 do
begin
Ob := TOb(ObjList[Index]);
TThread.CreateAnonymousThread(Ob.Go).Start;
end;
end;
The threads won't crash anymore, but they are likely to operate on the same TOb object, because CreateAnonymousThread() is taking a reference to the TOb.Go() method itself, and then your loop is modifying that reference's Self pointer on each iteration. I suspect the compiler is likely generating code similar to this:
var
Index: Integer;
Ob: TOb;
Proc: TProc; // <-- silently added
begin
for Index := 0 to ObjList.Count-1 do
begin
Ob := TOb(ObjList[Index]);
Proc := Ob.Go; // <-- silently added
TThread.CreateAnonymousThread(Proc).Start;
end;
end;
If you do this instead, it will have a similar issue:
procedure StartThread(Proc: TProc);
begin
TThread.CreateAnonymousThread(Proc).Start;
end;
...
var
Index: Integer;
Ob: TOb;
begin
for Index := 0 to ObjList.Count-1 do
begin
Ob := TOb(ObjList[Index]);
StartThread(Ob.Go);
end;
end;
Probably because the compiler generates code similar to this:
procedure StartThread(Proc: TProc);
begin
TThread.CreateAnonymousThread(Proc).Start;
end;
...
var
Index: Integer;
Ob: TOb;
Proc: TProc; // <--
begin
for Index := 0 to ObjList.Count-1 do
begin
Ob := TOb(ObjList[Index]);
Proc := Ob.Go; // <--
StartThread(Proc);
end;
end;
This will work fine, though:
procedure StartThread(Ob: TOb);
begin
TThread.CreateAnonymousThread(Ob.Go).Start;
end;
...
var
Index: Integer;
Ob: TOb;
begin
for Index := 0 to ObjList.Count-1 do
begin
Ob := TOb(ObjList[Index]);
StartThread(Ob);
// or just: StartThread(TOb(ObjList[Index]));
end;
end;
By moving the call to CreateAnonymousThread() into a separate procedure that isolates the actual reference to TOb.Go() into a local variable, you remove any chance of conflict in capturing the reference for multiple objects.
Anonymous procedures are funny that way. You have to be careful with how they capture variables.
After reading a the article that Remy Lebeau post on the comments, I found this solution.
changing the main object by add one more procedure that make the call.
Change the loop instead of creating the anonymous thread at the main loop, it is created inside the object.
TOb = class
Name : String;
constructor Create(Name : String);
procedure Process();
procedure DoWork();
end;
procedure TOb.Process;
begin
TThread.CreateAnonymousThread(DoWork).Start;
end;
procedure TOb.DoWork;
var
List : TStringList;
begin
List := TStringList.Create;
List.Add('I am ' + Name);
List.Add(DateTimeToStr(Now));
List.SaveToFile('D:\file_' + Name + '.txt');
List.Free;
end;
And the loop:
List := TObjectList<TOb>.Create();
List.Add(TOb.Create('Thread_A'));
List.Add(TOb.Create('Thread_B'));
List.Add(TOb.Create('Thread_C'));
List.Add(TOb.Create('Thread_D'));
for Obj in List do
//TThread.CreateAnonymousThread(Obj.Go).Start;
Obj.Process;
Thats resolves the problem with just a minimum change on the Main Object.
This about race condition. When you increased to max value to 100, you will see different values. Threading not guarantee when Thread starts or ends.
You can try this code block.
for I := 1 to 100 do
begin
TThread.CreateAnonymousThread(
procedure
var
Msg : string;
begin
try
Msg := 'This' + I.ToString;
MessageDlg(Msg,mtCustom,
[mbYes,mbAll,mbCancel], 0);
Except
on E: Exception do
End;
end
).Start;
end;
If you want a guarantee to write 1 to 4, you should instantiate every value before send to Thread.
for I := 1 to 100 do
begin
TThread.CreateAnonymousThread(
procedure
var
Msg : string;
begin
var instanceValue := I;
try
Msg := 'This' + instanceValue.ToString;
MessageDlg(Msg,mtCustom,
[mbYes,mbAll,mbCancel], 0);
Except
on E: Exception do
End;
end
).Start;
end;

Indexes don't work in FDQuery

I have a FDQuery that feeds data to a grid.
When the user clicks on a column I want the grid to order on that column.
Because I want to be able to sort on multiple columns, I cannot use the autosort option of the grid.
I tried the following code in my proof of concept.
However it does not work.
procedure TForm31.JvDBGrid1TitleBtnClick(Sender: TObject; ACol: Integer;
Field: TField);
const
sDesc = 1;
sASC = 2;
sNone = 0;
var
i: integer;
SortClause: string;
AField: TField;
AIndex: TFDIndex;
begin
case Field.Tag of
sDesc: Field.Tag:= sASC;
sASC: Field.Tag:= sNone;
sNone: Field.Tag:= sDesc;
end;
SortClause:= '';
FDQuery1.Indexes.BeginUpdate;
try
FDQuery1.Indexes.Clear;
for i:= 0 to JvDBGrid1.Columns.Count - 1 do begin
AField:= JvDBGrid1.Columns[i].Field;
if AField.Tag <> sNone then begin
AIndex:= FDQuery1.Indexes.Add;
AIndex.Name:= AField.FieldName;
AIndex.Fields:= AField.FieldName;
//AIndex.Options:= [soNoCase, soNullFirst, soDescNullLast, soDescending, soUnique, soPrimary, soNoSymbols]
case AField.Tag of
sDESC: AIndex.Options:= [soDescNullLast];
sASC: AIndex.Options:= [];
end;
AIndex.Active:= true;
end;
end;
finally
FDQuery1.Indexes.EndUpdate;
FDQuery1.Refresh;
end;
end;
It does not matter whether the Query already has an order by clause or not.
What am I doing wrong?
P.S. I'd rather not resort to constructing a custom order by clause but I know that's an option.
I think you may be missing a step, namely setting the FDQuery's IndexName to the name of the added index. Apparently. setting the added index's Active property is insufficient.
The following works fine for me against the MS Sql Server pubs database Authors table:
procedure TForm1.AddFDIndex;
var
AIndex : TFDIndex;
begin
AIndex := FDQuery1.Indexes.Add;
AIndex.Name := 'ByCityThenlname';
AIndex.Fields := 'city;au_lname';
AIndex.Active := True;
FDQuery1.IndexName := AIndex.Name;
end;
Btw, I'm not sure what your code is supposed to do if more than one column is tagged to be included in the index, but I'll leave that to you ;=)

Where is the 'EnablePinning' property in the ribbon framework's recent items?

The Windows ribbon framework markup supports an EnablePinning attribute for the recent items menu in the application menu:
<ApplicationMenu.RecentItems>
<RecentItems CommandName="MRU" EnablePinning="true" />
</ApplicationMenu.RecentItems>
I expected that there would be a matching property that can be queried/updated at runtime, but I can't find a property key. Does anyone know if there is one, and, if so, what it is?
Alternatively, is there another way to turn pinning on/off at runtime? Neither the element nor its parent support application modes.
TIA
Clarification: What I'm trying to do is enable/disable pinning for the entire menu at runtime. I'm not concerned about the pin states of the individual items.
I'm not sure if you can modify the pinned state from existing entries but it's definitely possible to programmatically query the state and add new items with a specific state using the UI_PKEY_Pinned property:
https://msdn.microsoft.com/en-us/library/windows/desktop/dd940401(v=vs.85).aspx
Wrappers such as the Windows Ribbon Framework for Delphi or the Windows Ribbon for WinForms (.NET) provide an easy access to the API model. This CodeProject article also describes how to query/add recent items using C#.
If you want to change the state during runtime, you could for example query the state of all items, remove them from the list, adjust whetever you need and add them to the list again. Didn't do that yet, could be worth a try however.
Hmm... this will be quite difficult to accomplish as the flag is defined in the XML which will be compiled into a resource file that is linked to the application and then loaded on start up. You could create another resource definition and reload the ribbon if you want to disable/enable the flagging, but that's quite a lot overhead and certainly noticeable from an users perspective as it requires the creation of a new window handle.
I place the recent items by inside UpdateProperty
TRecentItem = class(TInterfacedObject, IUISimplePropertySet)
private
FRecentFile: TSSettings.TRecentFile;
protected
function GetValue(const key: TUIPropertyKey; out value: TPropVariant): HRESULT; stdcall;
public
procedure Initialize(const RecentFile: TSSettings.TRecentFile); safecall;
end;
function TMyForm.UpdateProperty(commandId: UInt32; const key: TUIPropertyKey;
currentValue: PPropVariant; out newValue: TPropVariant): HRESULT;
var
I: Integer;
psa: PSafeArray;
pv: Pointer;
RecentItem: TRecentItem;
begin
if (key = UI_PKEY_RecentItems) then
begin
psa := SafeArrayCreateVector(VT_UNKNOWN, 0, Settings.RecentFiles.Count);
if (not Assigned(psa)) then
Result := E_FAIL
else
begin
for I := 0 to Settings.RecentFiles.Count - 1 do
begin
RecentItem := TRecentItem.NewInstance() as TRecentItem;
RecentItem.Initialize(Settings.RecentFiles[I]);
pv := Pointer(IUnknown(RecentItem));
Check(SafeArrayPutElement(psa, I, pv^));
end;
Result := UIInitPropertyFromIUnknownArray(UI_PKEY_RecentItems, psa, PropVar);
SafeArrayDestroy(psa);
end;
end;
If a pin was changed, I get this command while closing the application menu:
function TMyForm.Execute(commandId: UInt32; verb: _UIExecutionVerb;
key: PUIPropertyKey; currentValue: PPropVariant;
commandExecutionProperties: IUISimplePropertySet): HRESULT; stdcall;
var
Count: Integer;
I: Integer;
Pinned: Boolean;
psa: PSafeArray;
pv: IUnknown;
RecentFile: UInt32;
SimplePropertySet: IUISimplePropertySet;
Value: TPropVariant;
begin
if ((commandId = cmdAppRecentItems)
and Assigned(key) and (key^ = UI_PKEY_RecentItems)
and Assigned(currentValue) and (currentValue^.vt = VT_ARRAY + VT_UNKNOWN)) then
begin
psa := nil;
Result := UIPropertyToIUnknownArrayAlloc(key^, currentValue^, psa);
if (Succeeded(Result)) then
begin
Result := SafeArrayGetUBound(psa, 1, Count);
for I := 0 to Count do
if (Succeeded(Result)) then
begin
Result := SafeArrayGetElement(psa, I, pv);
if (Succeeded(Result) and Assigned(pv)) then
begin
Result := pv.QueryInterface(IUISimplePropertySet, SimplePropertySet);
if (Succeeded(Result)) then
Result := SimplePropertySet.GetValue(UI_PKEY_Pinned, Value);
if (Succeeded(Result)) then
Result := UIPropertyToBoolean(UI_PKEY_Pinned, Value, Pinned);
if (Succeeded(Result)) then
Settings.RecentFiles.SetPinned(I, Pinned);
end;
end;
SafeArrayDestroy(psa);
end;
end
end;
... but I didn't find a documentation of this solution.

Update fonts recursively on a Delphi form

I'm trying to iterate all the controls on a form and enable ClearType font smoothing. Something like this:
procedure TForm4.UpdateControls(AParent: TWinControl);
var
I: Integer;
ACtrl: TControl;
tagLOGFONT: TLogFont;
begin
for I := 0 to AParent.ControlCount-1 do
begin
ACtrl:= AParent.Controls[I];
// if ParentFont=False, update the font here...
if ACtrl is TWinControl then
UpdateControls(Ctrl as TWinControl);
end;
end;
Now, is there a easy way to check if ACtrl have a Font property so i can pass the Font.Handle to somethink like:
GetObject(ACtrl.Font.Handle, SizeOf(TLogFont), #tagLOGFONT);
tagLOGFONT.lfQuality := 5;
ACtrl.Font.Handle := CreateFontIndirect(tagLOGFONT);
Thank you in advance.
You use TypInfo unit, more specifically methods IsPublishedProp and GetOrdProp.
In your case, it would be something like:
if IsPublishedProp(ACtrl, 'Font') then
ModifyFont(TFont(GetOrdProp(ACtrl, 'Font')))
A fragment from one of my libraries that should put you on the right path:
function ContainsNonemptyControl(controlParent: TWinControl;
const requiredControlNamePrefix: string;
const ignoreControls: string = ''): boolean;
var
child : TControl;
iControl: integer;
ignored : TStringList;
obj : TObject;
begin
Result := true;
if ignoreControls = '' then
ignored := nil
else begin
ignored := TStringList.Create;
ignored.Text := ignoreControls;
end;
try
for iControl := 0 to controlParent.ControlCount-1 do begin
child := controlParent.Controls[iControl];
if (requiredControlNamePrefix = '') or
SameText(requiredControlNamePrefix, Copy(child.Name, 1,
Length(requiredControlNamePrefix))) then
if (not assigned(ignored)) or (ignored.IndexOf(child.Name) < 0) then
if IsPublishedProp(child, 'Text') and (GetStrProp(child, 'Text') <> '') then
Exit
else if IsPublishedProp(child, 'Lines') then begin
obj := TObject(cardinal(GetOrdProp(child, 'Lines')));
if (obj is TStrings) and (Unwrap(TStrings(obj).Text, child) <> '') then
Exit;
end;
end; //for iControl
finally FreeAndNil(ignored); end;
Result := false;
end; { ContainsNonemptyControl }
There's no need to use RTTI for this. Every TControl descendant has a Font property. At TControl level its visibility is protected but you can use this workaround to access it:
type
THackControl = class(TControl);
ModifyFont(THackControl(AParent.Controls[I]).Font);
One other thing worth mentioning. Every control has a ParentFont property, which - if set - allows the Form's font choice to ripple down to every control. I tend to make sure ParentFont is set true wherever possible, which also makes it easier to theme forms according to the current OS.
Anyway, surely you shouldn't need to do anything to enable ClearType smoothing? It should just happen automatically if you use a TrueType font and the user has enabled the Cleartype "effect".
Here's a C++Builder example of TOndrej's answer:
struct THackControl : TControl
{
__fastcall virtual THackControl(Classes::TComponent* AOwner);
TFont* Font() { return TControl::Font; };
};
for(int ControlIdx = 0; ControlIdx < ControlCount; ++ControlIdx)
{
((THackControl*)Controls[ControlIdx])->Font()->Color = clRed;
}

Resources