How can I remove a PCB Object in Altium PCB Library using Altium scripting systems? - delphi

I am writing a Delphi Altium script to remove all tracks in TopOverLay inside the PCB Library.
When running the script though, nothing happens (the tracks are not removed).
I don't know why. Can you please help me?
Here below is my code :
procedure RemoveTrackObject;
Var
MyComponent : IPCB_LibComponent;
MyTrack : IPCB_Track;
Iterator : IPCB_GroupIterator;
DeleteList : TInterfaceList;
TrackTemp : IPCB_Track;
i : Integer;
begin
MyComponent := PCBServer.GetCurrentPCBLibrary.CurrentComponent;
//////////////////////////////////////////////////////////////////////
Iterator := MyComponent.GroupIterator_Create;
Iterator.AddFilter_ObjectSet(Mkset(eTrackObject));
Iterator.AddFilter_LayerSet(Mkset(eTopOverLay));
DeleteList := TInterfaceList.Create;
try
MyTrack := Iterator.FirstPCBObject;
While MyTrack <> nil do
begin
DeleteList.Add(MyTrack);
MyTrack := Iterator.NextPCBObject;
end;
finally
MyComponent.GroupIterator_Destroy(Iterator);
end;
try
PCBServer.PreProcess;
for i := 0 to DeleteList.Count - 1 do
begin
TrackTemp := DeleteList.Items[i];
MyComponent.RemovePCBObject(TrackTemp);
end;
finally
PCBServer.PostProcess;
DeleteList.Free;
end;
Client.SendMessage('PCB:Zoom', 'Action=Redraw' , 255, Client.CurrentView);
end;

AFAIU In Altium dephiscript API InterfaceList have specific uses: holding non-PCB objects & passing to external dll functions & letting the receiving fn destroy the list.
You don't really need one here.
The PcbLib does have some strange behaviour around deleting from selected/focused footprint etc.
I think the problem is caused by the Pcb Editor not allowing objects to be deleted from the current focused component/footprint.
The history around this issue points to solutions involving moving focus away from the required component..
You can't complete the delete process while the List still contains the object reference.
Use a While loop, after RemovePCBObject(), remove object ref from the List (remove the List Item). Then when the While loop terminates you have zero items in List.
Might help refresh or look & feel to use some of these fn calls:
CurrentLib.Board.GraphicallyInvalidate;
CurrentLib.Navigate_FirstComponent;
CurrentLib.Board.ViewManager_FullUpdate;
CurrentLib.Board.GraphicalView_ZoomRedraw;
CurrentLib.RefreshView;

Related

What is the best way in Delphi to generate a list of forms/units in the order they load?

I had an issue where a file kept deleting on startup and I couldn't track down the code responsible. I wound up adding Vcl.Dialogs to all the units and creating an initialization section that looked like this:
initialization
begin
ShowMessage('Inside [Unit Name Here]');
end;
This was quite a pain. Is there an easy way to generate a list of forms/units in the order in which they fire off?
UPDATE: 2019-08-01 (Helpful MAP links)
Here are two links that may assist in understanding DELPHI map files
http://docwiki.embarcadero.com/RADStudio/Rio/en/API_%28%2A.map%29
Understanding Delphi MAP File
You really didn't need to go to all that trouble modifying your source units. I think you'll find that using the method below will find the misbehaving unit
much more quickly than somehow generating a list of units and then ploughing
your way through it.
If you look in System.Pas, you'll find a procedure InitUnits like this (from D7).
procedure InitUnits;
var
Count, I: Integer;
Table: PUnitEntryTable;
P: Pointer;
begin
if InitContext.InitTable = nil then
exit;
Count := InitContext.InitTable^.UnitCount;
I := 0;
Table := InitContext.InitTable^.UnitInfo;
[...]
try
while I < Count do
begin
P := Table^[I].Init;
Inc(I);
InitContext.InitCount := I;
if Assigned(P) then
begin
TProc(P)();
end;
end;
except
FinalizeUnits;
raise;
end;
end;
This is the code which causes the initialization code of each unit to be called. It works its way through the units and calls the initialization section (if any)
of each unit via the call
TProc(P)();
You can inspect the value of Count prior to the loop; don't be surprised if its upwards
of a couple of hundreds even for a relatively simple project.
Put a breakpoint on the TProc(P)(); line and right-click and set the PassCount to
half the value of Count. Run your app and when the breakpoint trips, check whether
the file has been deleted.
You can then do a binary search through the values of
Count (by continuing the current run if the file is still there, or resetting the app
and halving the Pass Count) to establish exactly which unit causes the file to be deleted.
Because you can use a binary search to do this, it will rapidly converge on the
unit which is deleting the file. Of course, you can trace into the unit's
initialization code (if it has been compiled with debug info) when the breakpoint
trips by pressing F7 on TProc(P)();
You can inspect the segments section of the map file. The entries with C=ICODE are those units with initialization parts in the order they are executed.

What is a better method to suspend program execution until a condition is met?

I need to wait until a mapped network folder (\HostName\NetworkPath) become empty. What I mean is that program flow cannot continue until that network folder is empty.
So far I have the following logic in place but I noticed that it takes time before FindFirst notices that the network folder become empty.
If I keep observing an opened explorer windows, pointing to that network folder, I notice that it become empty far before FindFirst notices it.
I used Sleep(5000) to introduce some delay in calling again CheckNetworkFolderIsEmpty in my while loop, otherwise it is being called too often. But maybe that folder will become empty far before 5 seconds, so 5 seconds is an arbitrary time delay that may results in an unnecessary dealy in program execution, in the event that the folder become empty before.
What can be the culprit, what can be a better alternative?
Also I do not know what else to use instead of a simple Sleep.
while not CheckRawFolderIsEmpty do begin
Sleep(5000);
end;
function TForm1.CheckNetworkFolderIsEmpty: Boolean;
begin
Result := (CountFilesInFolder('\\HostName\NetworkPath', '*.txt') = 0);
end;
function CountFilesInFolder(const aPath, aFileMask: string): Integer;
var
Path: string;
SearchRec: TSearchRec;
begin
Path := IncludeTrailingPathDelimiter(aPath);
Result := 0;
if FindFirst(Path + aFileMask, faAnyFile and not faDirectory, SearchRec) = 0 then begin
repeat
Inc(Result);
until FindNext(SearchRec) <> 0;
FindClose(SearchRec);
end;
end;
Observing file system changes like you do is inefficient (FindFirst, FindNext) and inacurate as you've learned. Windows provides API FindFirstChangeNotification for that purpose as J... has pointed out in the comment under your question.
Good news is that you don't need to start studying the API from scratch, because some other people did the hard work for you. Check out some freeware wrappers for Delphi around the API:
https://torry.net/pages.php?id=252
http://www.angusj.com/delphi/dirwatch.html
...

Odd behaviour when adding a toolbutton to the delphi ide

I was trying out some things and wanted to make a delphi IDE extension.
My basic idea was expanding the ToDo list feature that is currently in the IDE.
Step one was adding a toolbutton to the IDE which would open a form showing the todo items.
But I noticed some weird things that I hopefully caused myself since that would mean it can be easily fixed.
I am adding my toolbutton to the CustomToolbar, which is the one with the blue questionmark (see screenshot later)
The thing that happens: I install my package and the button is added with the correct image, right next to the existing button.
Now I close the modal form with the installed packages and then the blue questionmark changes.
Don't mind the icon I used, I will use a different one eventually but ok.
So basicly the existing item changes to my own icon but disabled for some reason. And I can't figure out why this happens.
As suggested in the guide I found online I used a TDatamodule to implement my code.
My code:
procedure TDatamoduleToDoList.Initialize;
var
LResource, LhInst: Cardinal;
begin
LhInst := FindClassHInstance(Self.ClassType);
if LhInst > 0 then
begin
LResource := FindResource(LhInst, 'icon', RT_Bitmap);
if LResource > 0 then
begin
FBMP := Vcl.Graphics.TBitmap.Create;
FBMP.LoadFromResourceName(LhInst, 'icon');
end
else
DoRaise('Resource not found');
end
else
DoRaise('HInstance Couldn''t be found');
FToDoAction := TTodoAction.Create(Self);
FToDoAction.Category := actionCat;
FToDoAction.ImageIndex := FIntaServices.ImageList.Add(FBMP, nil);
FToDoAction.Name := 'my_very_own_action_man';
end;
procedure TDatamoduleToDoList.DataModuleCreate(Sender: TObject);
begin
//Create extension
if Supports(BorlandIDEServices, INTAServices, FIntaServices) then
begin
Initialize;
if FToDoAction <> nil then
FCustBut := TSpeedButton(FIntaServices.AddToolButton(sCustomToolBar, 'CstmToDoList', FToDoAction))
else
DoRaise('Initialize failed');
end
else
DoRaise('Something went wrong');
end;
DoRaise is my own procedure that simply destroys all of my objects and raises an exception, did this to prevent mem leaks in the ide.
But, I think, I don't do anything weird but yet this problem occurs.
So I'm hoping someone here might have done something simular and sees the error in my code.
Thanks in advance.
P.s. if you need any more info or see the rest of the unit let me know and ill put the entire unit on github or something like that.
Edit:
Thanks to #Uwe Raabe I managed to solve this problem.
The problem was found in the comments of INTAServices.AddImages
AddImages takes all the images from the given image list and adds them
to the
main application imagelist. It also creates an internal mapping array from the
original image indices to the new indices in the main imagelist. This
mapping is used by AddActionMenu to remap the ImageIndex property of the
action object to the new ImageIndex. This should be the first method
called when adding actions and menu items to the main application window.
The return value is the first index in the main application image list of
the first image in the source list. Call this function with an nil
image list to clear the internal mapping array. Unlike the AddImages function from
the ancestor interface, this version takes an Ident that allows the same base index
to be re-used. This is useful when the IDE implements demand-loading of
personalities so that the images will only get registered once and the same image
indices can be used.
The solution eventually was adding my image to a local imagelist which was added to the imagelist of IntaServices
Code:
procedure TDatamoduleToDoList.DataModuleCreate(Sender: TObject);
begin
//Create extension
if Supports(BorlandIDEServices, INTAServices, FIntaServices) then
begin
Initialize;
if FToDoAction <> nil then
begin
FCustBut := TSpeedButton(FIntaServices.AddToolButton(sCustomToolBar, 'CstmToDoList', FToDoAction));
FToDoAction.ImageIndex := FIntaServices.AddImages(FImages);//This is the fix
end
else
DoRaise('Initialize failed');
end
else
DoRaise('Something went wrong');
end;
You are not supposed to fiddle around with the INTAServices.ImageList directly. Instead use either INTAServices.AddMasked or INTAServices.AddImages (in case you have a local imagelist in your datamodule).
You can safely use the INTAServices.ImageList to be connected to your controls, but you should neither Add nor Delete the images in it directly.

Error when using parameter in ADOQuery

I have this simple code to check if a record exists in a table, but it always returns a runtime error :
Arguments are of the wrong type, are out of acceptable range, or are
in conflict with one another.
my code is this :
function TDataModuleMain.BarCodeExists(barCode: string): boolean;
begin
if ADOQuerySql.Active then
ADOQuerySql.Close;
ADOQuerySql.SQL.Clear;
ADOQuerySql.SQL.Text := 'select count(1) from Card where BarCode = (:TestBarcode)';
ADOQuerySql.Parameters.ParamByName('TestBarcode').Value := barCode;
ADOQuerySql.Open; // HERE THE RUNTIME ERROR APPEARS
Result := ADOQuerySql.Fields[0].AsInteger = 1;
ADOQuerySql.Close;
ADOQuerySql.Parameters.Clear;
end;
The field BarCode in table Card is of type nvarchar(100)
In debug I see that the parameter is created, and gets populated with the correct value.
Running the query in sql server management studio also works.
I also found this How to pass string parameters to an TADOQuery? and checked my code with the code in the answer but I don't see any problems here.
Also this AdoQuery Error using parameters did not help me.
It will no doubt be something very simple that I have missed but I just dont see it now.
EDIT : things I tried from suggestions in the comments:
.ParamCheck := True (default)
.Parameters.ParamByName('TestBarcode').DataType := ftString
.Parameters.ParamByName('TestBarcode').DataType := ftWideString
None of these worked however.
What did help was using a non-shared AdoQuery for this, and that one did the job without any errors. I am using that now as the solution but I am still looking at the shared AdoQuery out of curiousity what the exact problem is.
EDIT: the source of the problem is found.
I used the function provided by MartinA to examine both the dynamic created query and the shared AdoQuery and I found one difference.
The shared AdoQuery had the this property filled :
ExecuteOption := [eoExecuteNoRecords]
and the dynamic created query does not.
Since this property is not set in designtime I did not see it.
After clearing the property to [] the shared AdoQuery worked again.
I am going to switch to using non shared AdoQuery for this kind of work as been suggested.
Thanks everyone for your assistance.
The following isn't intended to be a complete answer to your q, but to follow up my comment that "all you have to do is to inspect your form's DFM and compare the properties of your original ADoQuery with the unshared one. The answer should lie in the difference(s)" and your reply that the unshared query is created dynamically.
There is no "voodoo" involved in the difference in behaviour between your two ADOQuerys. It's just a question of capturing what the differences actually are.
So, what you need, to debug the problem yourself, is some code to compare the properties of two components, even if one or both of them is created dynamically. Using the following routine on both components will enable you to do exactly that:
function TForm1.ComponentToString(AComponent : TComponent) : String;
var
SS : TStringStream;
MS : TMemoryStream;
Writer : TWriter;
begin
// Note: There may be a more direct way of doing the following, without
// needing the intermediary TMemoryStream, MS
SS := TStringStream.Create('');
MS := TMemoryStream.Create;
Writer := TWriter.Create(MS, 4096);
try
Writer.Root := Self;
Writer.WriteSignature;
Writer.WriteComponent(AComponent);
Writer.FlushBuffer;
MS.Position := 0;
ObjectBinaryToText(MS, SS);
Result := SS.DataString;
finally
Writer.Free;
MS.Free;
SS.Free;
end;
end;
Over to you ...

Delphi Pascal Problem when WMDeviceChange function calls other functions/procedures

SOLVED
I am using delphi 2009. My program listens for usb drives being connected and remove. Ive used a very similar code in 10 apps over the past year. It has always worked perfectly. When i migrated i had to give up using thddinfo to get the drive model. This has been replaced by using WMI. The WMI query requires the physical disk number and i happen to already have a function in the app for doing just that.
As i test I put this in a button and ran it and it successfully determines the psp is physical drive 4 and returns the model (all checked in the debugger and in another example using show message):
function IsPSP(Drive: String):Boolean;
var
Model: String;
DriveNum: Byte;
begin
Result := False;
Delete(Drive, 2, MaxInt);
DriveNum := GetPhysicalDiskNumber(Drive[1]);
Model := (MagWmiGetDiskModel(DriveNum));
if Pos('PSP',Model) > 0 then Result := True;
end;
procedure TfrmMain.Button1Click(Sender: TObject);
var DriveNum: Byte;
begin
IsPSP('I');
end;
It works perfectly that is until i allow the WMDeviceChange that ive been using for a year to call up the getphysicaldisknumber and the wmi query statement. Ive tried them by themselves theyre both a problem. GetPhysicalDiskNumber freezes real bad when its doing a CloseHandle on the logical disk but does return the number eventually. The WMI query fails with no error just returns '' debugger points into the wbemscripting_tlb where the connection just never happened. Keep in mind the only thing thats changed in a year is what im calling to get the model i was using an api call and now im using something else.
Below is the rest of the code involved at this time sans the ispsp that is displayed above:
procedure TfrmMain.WMDeviceChange(var Msg: TMessage);
var Drive: String;
begin
case Msg.wParam of
DBT_DeviceArrival: if PDevBroadcastHdr(Msg.lParam)^.dbcd_devicetype = DBT_DevTyp_Volume then
begin
Drive := GetDrive(PDevBroadcastVolume(Msg.lParam)) + '\';
OnDeviceInsert(Drive);
end;
DBT_DeviceRemoveComplete: if PDevBroadcastHdr(Msg.lParam)^.dbcd_devicetype = DBT_DevTyp_Volume then
begin
Drive := GetDrive(PDevBroadcastVolume(Msg.lParam)) + '\';
OnDeviceRemove(Drive);
end;
end;
end;
Procedure TfrmMain.OnDeviceInsert(Drive: String);
var PreviousIndex: Integer;
begin
if (getdrivetype(Pchar(Drive))=DRIVE_REMOVABLE) then
begin
PreviousIndex := cbxDriveList.Items.IndexOf(cbxDriveList.Text);
cbxDriveList.Items.Append(Drive);
if PreviousIndex = -1 then //If there was no drive to begin with then set index to 0
begin
PreviousIndex := 0;
cbxDriveList.ItemIndex := 0;
end;
if isPSP(Drive) then
begin
if MessageDlg('A PSP was detect # ' + Drive + #10#13 + 'Would you like to select this drive?',mtWarning,[mbYes,mbNo], 0) = mrYes then
cbxDriveList.ItemIndex := cbxDriveList.Items.IndexOf(Drive)
else cbxDriveList.ItemIndex := PreviousIndex;
end
else if MessageDlg('USB Drive ' + Drive + ' Detected' + #10#13 + 'Is this your target drive?',mtWarning,[mbYes,mbNo], 0) = mrYes then
cbxDriveList.ItemIndex := cbxDriveList.Items.IndexOf(Drive)
else cbxDriveList.ItemIndex := PreviousIndex;
end;
end;
Procedure TfrmMain.OnDeviceRemove(Drive: String);
begin
if not (getdrivetype(Pchar(Drive)) = DRIVE_CDROM) then
begin
if cbxDriveList.Text = (Drive) then ShowMessage('The selected drive (' + Drive + ') has been removed');
cbxDriveList.Items.Delete(cbxDriveList.Items.IndexOf(Drive));
if cbxDriveList.Text = '' then cbxDriveList.ItemIndex := 0;
if Drive = PSPDrive then //Check Detect PSP and remove reference if its been removed
begin
PSPDrive := '';
end;
end;
end;
Rob has said something below about im not calling the inherited message handler, ive read the document i see a couple of things i can return... but im not really sure i understand but i will look into it. Im not a very good pascal programmer but ive been learning alot. The transition to 2009 has had some rough patches as well.
The USB drive detection and all that works perfectly. If i remove the two things from is psp the user is greeted right away with wis this your whatever and adds I:\ to the list. Its just the two new things that have changed in the app that fail when called by wmdevicechange and as said before they work on their own.
EDIT - SOLVED
Alright well im using a timer as suggested and the problem seems to be solved. One note is that when called by the timer very shortly after the wmdevicechange getting the physical disk number still seems to be slow. I attribute this to the device still being attached to the system.
On that note im using a P2 450 on the regular. I hooked the PSP and app to a 1.8Ghz Dual Core Laptop and the program detected the psp and notified the user very fast. So the app wont freeze unless there on a very very slow computer and on this slow onw its only for a matter of seconds and doesnt affect the operation of the program though isnt very cool. But i feel that all modern computers will run the detection fast especially because they can attach the device alot faster.
It's possible that the information you're querying becomes available only after the WMDeviceChange message handler runs. If the very same code works when called from a button, try this:
Refactor your WMDeviceChange handler code into one or more separate methods.
In the WMDeviceChange handler, activate a precreated timer and have it fire one second later, or something like that.
Call the former WMDeviceChange handler code from the timer handler code.
You haven't indicated what "statement 1" is in your code.
I have a few comments about parts of the code, which may or may not be related to the problem you're having.
First, you assign a value to DriveNum in IsPSP, but you don't use it. The compiler should have issued a hint about that; don't ignore hints and warnings. You also pass the magic number 4 into MagWmiGetDiskModel; was that supposed to be DriveNum instead?
You aren't calling the inherited message handler, and you aren't returning a result in your message handler. The documentation tells what values you're supposed to return. To return a value from a Delphi message handler, assign a value to the Msg.Result field. For the cases that your message handler doesn't handle, make sure you call inherited so that the next handler up the chain can take care of them. If there is no next handler, then Delphi will call DefWindowProc to get the operating system's default behavior.
The change you've illustrated is called refactoring, and it will do nothing to affect how your code runs. It makes the code easier to read, though, so please keep the second version. As for finding the problem, my best advice is to use the debugger to step through the code to identify the point where things stat to go wrong and the parts that run slower than you'd like. You can also try removing portions of the code to confirm that the other parts work correctly in isolation.

Resources