I'm writing a small Delphi 10.3 VCL Win32 application for viewing a bunch of SVG images. Yes, there must be application that does exactly what I want but I'm using this as a bit of a learning opportunity.
It renders each image and a label indicating the image name so you can quickly scroll through them.
I decided to thread it so the user doesn't need to wait until all the images have been loaded.
This was nice except that the main form isn't painting or processing user "messages" while the loading occurs.
I'm calling the method below via the thread's Synchronize( ) method.
procedure TFinderThread.CreateControlsForNextItem;
var
img: TcxImage;
begin
svg:=TdxSmartImage.Create;
svg.LoadFromFile(m_slFiles[0]);
m_slFiles.Delete(0);
// omitted: TLabel creation
img := TcxImage.Create(m_owner);
img.Left := 0;
img.Top := m_iNextControlTop;
img.Picture.Assign(svg);
img.Parent := m_parent;
m_iNextControlTop:=m_iNextControlTop + img.Height;
svg.Free;
end;
I'd like the main form to be responsive and usable while it's being loaded. I found that a short call to Sleep( ) in the thread's Execute loop makes the UI responsive.
the Execute method contains this loop:
m_iNextControlTop:=0;
while m_slFiles.Count>0 do
begin
if Terminated then
break;
Synchronize(CreateControlsForNextItem);
sleep(1);
end;
The problem
If you use the scroll bar during refresh, some controls don't appear at all in the scroll box. The objects exist, are visible, have the correct parent, etc. This is doesn't change if you later scroll around so it doesn't appear to be a "painting" issue. It's as though the scrollbox doesn't recognize them as content.
If you don't scroll at all during refresh, all the images will appear correctly.
Any idea what's happening?
Thanks!
Related
I have an Excel AddIn written in Delphi that has a VCL form with a TMemo on it.
When I try to enter text into the Memo the input goes to Excel instead.
When I start the form modal (ShowModal), all works fine but obviously it's not possible to work with the main excel window and the addin's window concurrently.
The issue seems to be the exact similar to this question: Modeless form cannot receive keyboard input in Excel Add-in developed by Delphi
This answer suggests to handle WM_PARENTNOTIFY so I tried the following:
TMyForm = class(TForm)
...
procedure OnParentNotify(var Msg: TMessage); message WM_PARENTNOTIFY;
And in that procedure tried things like SetFocus, WinApi.Windows.SetFocus(self.Handle), SetForeGroundWindows, SetActiveWindow but that doesn't appear to work.
Other suggestions I've read is to run the UI in a different thread (which is of course not possible with VCL) and to install a keyboard hook with SetWindowsHookEx. Obviously that will give us keypress events but not sure what to do with those.
I am not using 3rd party tooling such as Add-In Express but just implementing IDTExtensibility2.
EDIT: more research suggests that Office uses an interface called IMsoComponent and and IMsoComponentManager as a way of tracking the active component in the application. Visual Studio uses these as IOleComponent and IOleComponentManager.
This link and this one suggest to register a new empty IOleComponent/IMsoComponent.
EDIT: MCVE can be fetched here, it's the smallest possible Excel AddIn code that will launch a VCL Form with a TEdit on it. The edit looses keyboard focus as soon as a worksheet is active.
I was having the same kind of problem. I am also implementing IDTExtensibility2 but as I am doing it on C++ I already managed to run the UI on a different thread. But anyway I was not fully happy with this solution. I would still have this problem if I wanted to use a VBA Userform as a TaskPane Window. I did try but as (I guess, didn´t check) the VBA userform will run on the native Excel Thread, just calling it on a different thread (to use as a TaskPane window) just marshalled it, didn´t mean that it was created on a different thread, so as I did try, there was this kind of problem.
I too did read and try to to handle WM_PARENTNOTIFY messages with SetFocus.. on my window but didn´t work.
This both interfaces IOleComponent and IOleComponentManager were new to me. Didn´t find the header files, but could write and implement from the descriptions at the link you shared.
How it worked for me was to register my IOleComponent implementation on every WM_SETCURSOR e WM_IME_SETCONTEXT at my Form Window. (I am not sure if this is exactly the best messages, but did work for me) and Revoke the component on every click back at EXCEL7 window.
The MSOCRINFO options I used to register was msocrfPreTranslateKey and msocadvfModal.
Hope that with this answer I will not receive tons of criticism. I know that it is a very specific issue, the question was with a -1 status when I read it, but was exactly what I needed to finish with this point. So I am just trying to be honest and share back something.
I finally found the solution to this after I decided to have another look at this...
Seems I was on the right track about needing IMsoComponentManager and IMsoComponent.
So first we need to retrieve the ComponentManager:
function GetMsoComponentManager(out ComponentManager: IMsoComponentManager): HRESULT;
var
MessageFilter: IMessageFilter;
ServiceProvider: IServiceProvider;
begin
MessageFilter := nil;
// Get the previous message filter by temporarily registering a new NULL message filter.
Result := CoRegisterMessageFilter(nil, MessageFilter);
if Succeeded(Result) then
begin
CoRegisterMessageFilter(MessageFilter, nil);
if (MessageFilter <> nil) then
begin
try
ServiceProvider := MessageFilter as IServiceProvider;
Result := ServiceProvider.QueryService(IID_IMsoComponentManager,
SID_SMsoComponentManager, ComponentManager);
if Assigned(ComponentManager) then
begin
end;
except
on E: Exception do
begin
Result := E_POINTER;
end;
end;
end;
end;
end;
Then we need to register a dummy component using msocrfPreTranslateAll (or msocrfPreTranslateKey)
procedure TVCLForm.RegisterComponent;
var
RegInfo: MSOCRINFO;
//MsoComponentManager: IMsoComponentManager;
hr: HRESULT;
bRes: Boolean;
begin
if FComponentId = 0 then
begin
FDummyMsoComponent := TDummyMsoComponent.Create;
ZeroMemory(#RegInfo, SizeOf(RegInfo));
RegInfo.cbSize := SizeOf(RegInfo);
RegInfo.grfcrf := msocrfPreTranslateAll or msocrfNeedIdleTime;
RegInfo.grfcadvf := DWORD(msocadvfModal);
bRes := ComponentManager.FRegisterComponent(FDummyMsoComponent, RegInfo,
FComponentId);
Memo1.Lines.Add(Format('FMsoComponentManager.FRegisterComponent: %s (Component ID: %d)', [BoolToStr(bRes, True), FComponentId]));
end
else begin
Memo1.Lines.Add(Format('Component with ID %d was already registered', [FComponentId]));
end;
if FComponentId > 0 then
begin
bRes := ComponentManager.FOnComponentActivate(FComponentId);
Memo1.Lines.Add(Format('FMsoComponentManager.FOnComponentActivate: %s (Component ID: %d)', [BoolToStr(bRes, True), FComponentId]));
end;
end;
Now in the Dummy Component implementation class we must handle FPreTranslateMessage:
function TDummyMsoComponent.FPreTranslateMessage(MSG: pMsg): BOOL;
var
hWndRoot: THandle;
begin
// this is the magic required to make sure non office owned windows (forms)
// receive Window messages. If we return True they will not, however if we
// return False, they will -> so we check if the message was meant for the
// window owner
hWndRoot := GetAncestor(MSG^.hwnd, GA_ROOT);
Result := (hWndRoot <> 0) and (IsDialogMessage(hWndRoot, MSG^));
end;
Finally a good place to to (un)register the Dummy component is when receiving WM_ACTIVATE. For example:
procedure TVCLForm.OnActivate(var Msg: TMessage);
var
bRes: Boolean;
begin
case Msg.WParam of
WA_ACTIVE:
begin
Memo1.Lines.Add('WA_ACTIVE');
RegisterComponent;
end;
WA_CLICKACTIVE:
begin
Memo1.Lines.Add('WA_CLICKACTIVE');
RegisterComponent;
end;
WA_INACTIVE:
begin
Memo1.Lines.Add('WA_INACTIVE');
UnRegisterComponent;
end
else
Memo1.Lines.Add('OTHER/UNKNOWN');
end;
end;
This all seems to work well and does not require intercepting WM_SETCURSOR or WM_IME_SETCONTEXT nor does it need subclassing of the Excel Window.
Once cleaned up will probably write a blog and place all the complete code on Github.
Given this scenario:
procedure TForm2.Button1Click(Sender: TObject);
var
Form: TForm;
begin
Form := TForm.Create(nil);
Form.OnClose := FormClosed;
Form.Show;
Sleep(200);
TThread.CreateAnonymousThread(
procedure
begin
TThread.Synchronize( nil,
procedure
begin
Form.Close;
MessageDlg('Testing', mtInformation, [mbok], 0);
end);
end).Start;
end;
procedure TForm2.FormClosed(Sender: TObject; var Action: TCloseAction);
begin
Action := TCloseAction.caFree;
end;
My MessageDlg call is not displayed (the result of this call is always mrCancel (2)).
After digging around, it's related to the OnClose event and setting the Action to caFree.
Changing Form.Close to Form.Free and removing the OnClose event entirely displays MessageDlg ok. Placing MessageDlg before the call to Form.Close works ok. Originally I thought scope of my Form variable might have caused the problem but declaring Formas a private field in TForm2 instance doesn't solve the problem.
My aim was to display a splash form, execute my thread, then through call backs of said thread, close the Splash form and display dialog where appropriate to the user.
For clarity, why does this occur?
What is happening is that the dialog's owning window is the form that is being closed. When the dialog starts its modal message loop, the form is released and takes down its owned windows with it. Including the dialog.
Test this out, to give you more confidence that what I state above is correct, by replacing the call to show the dialog first with
MessageBox(0, ...);
and then with
MessageBox(Form.Handle, ...);
That is, be explicit about the dialog's owner.
The first version, with no owner, will show the dialog. The second won't because it replicates the scenario in your code.
The Windows runtime requires that the messages for a visual window be processed by a message loop running in the same thread that creates that window.
The Windows API's also enforce rules about what operations can be performed on a window from a thread other than that in which the window was created. i.e. very little indeed, other than sending or posting messages to it.
With that information in mind, we can explain what is occurring in your case.
The Form.Close method ultimately closes a form by posting it a message (CM_RELEASE). But in your implementation the message loop responsible for responding to that message - the application main message loop - is blocked as a result of the fact that the message is posted from within a Synchronize() method.
i.e. your Synchronize()d method posts the message to close the form, but that message cannot and will not be processed by that forms window until your Synchronize()d method has completed, and that will not occur until the user has responded to the message box you are presenting in that method.
I hope that helps you understand what is going on in your code.
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 8 years ago.
Improve this question
i need to get the current select text independently the windows (browser, text editor, games wtc.)
i found this code in stackoverflow, but it aint work.
...
var
Buff: array[0..65535] of char;
...
function CurrentSelectedText: string;
var
hFocus: hWnd;
aStart, aEnd: integer;
begin
//added by andrei, attach input to current thread
AttachThreadInput(GetCurrentThreadId, GetWindowThreadProcessId(GetForegroundWindow), true);
hFocus:= GetFocus;
SendMessage(hFocus, WM_GETTEXT, 65535, integer(#buff));
SendMessage(hFocus, EM_GETSEL, Integer(#aStart), Integer(#aEnd));
result:= Copy(StrPas(Buff), 1+aStart, aEnd-aStart);
end;
There are some possibly significant problems (failing to detach the thread input, not allowing for the foreground window in the current thread, a complete absence of error checking) and a minor improvement (caching thread IDs) that can be made to your code, but in essence it should work.
I implemented this slightly modified version of your code in a TTimer, set to simply retrieve the current focussed window text into a TEdit control every 1/4 second (250 ms), and it worked exactly as expected.
var
buff: array[0..65535] of char;
thisThread: Integer;
focusThread: Integer;
hForeground: HWND;
hFocus: HWND;
a, b: Integer;
attached: Boolean;
begin
hForeground := GetForegroundWindow;
// ** check hForegound is valid
thisThread := GetCurrentThreadID;
focusThread := GetWindowThreadProcessID(hForeground);
// ** check focusThread is valid
attached := thisThread <> focusThread;
if attached then
begin
AttachThreadInput(thisThread, focusThread, TRUE);
// ** check attach was successful
end;
try
hFocus := GetFocus;
// ** check hFocus is valid/not null
SendMessage(hFocus, WM_GETTEXT, 65535, Integer(#buff));
// ** check SendMessage was successful
Edit1.Text := String(#buff);
if SendMessage(hFocus, EM_GETSEL, Integer(#a), Integer(#b)) <> 0 then
Edit1.Text := Copy(Edit1.Text, 1 + a, b - a);
finally
if attached then
AttachThreadInput(thisThread, focusThread, FALSE);
end;
end;
If this code does not work for you then you can (and in any case should) add error checking code at the points that I have indicated with ** comments, to determine why it is not working in your particular case.
Note that you cannot attach a thread to itself, which is taken into account in the above modified code by only attaching (and detaching) if the two threads are different.
The EM_GETSEL Problem:
It is most likely possible that the problem you have is specifically with the attempt to retrieve the Selection range. As indicated by the message number (EM_...) this is a message that only EDIT controls respond to, so if the current foreground window is not an edit control then this will almost certainly fail.
You should DEFINITELY check the success/failure of the SendMessage( .. EM_GETSEL .. ) call, and only extract a range from the window text if you receive a valid response to that message.
I also found that trying to use Copy() directly on a cast version of the buf char array did not work. This surprised me and might bear further investigation, but to avoid whatever problems might lie in this area, simply convert to a string first and then perform your Copy() to extract the range from the string.
In my working example above this is achieved by rather inefficiently assigning the converted buf to the Edit1.Text property and then using Copy() on that Edit1.Text property. In practice you would use an intermediate string variable.
WM_GETTEXT Limitations
In your question you ask for a technique that will work for any window type, including browsers and games. I do not think there is a universal approach that will achieve this, short of a combination of screen capture and OCR. This is because applications such as browsers and games and even some "ordinary applications", implement window classes which render their content independent of any concept of "Window Text", as reported by WM_GETTEXT. e.g. the "Window text" of a browser window is typically the browser/tab caption text, not the HTML content of the page it is currently presenting, which is maintained internally by the browser application.
If an application implements a custom window class to render some arbitrary content, then you have no way to retrieve that content without intimate knowledge of the specific application in question and/or a published mechanism supported by that application to achieve what you want.
I have a question about threads and controls. I made a syncing modal dialog. There's three progressbars and couple of captions. If application is opened for a first time, then it will open syncing dialog and do the update thing. On dialog Show() method I create three different threads. Each thread controls data download, xml parsing and database inserting. And then shows progress on progressbar. All described, is working fine.
Now the problem - I want to close this dialog automatically when all items are downloaded, parsed and inserted to database. I tried to check if progressbar.position equals to progressbar.max and I tried check if threads are terminated.If I go with the progressbar way, dialog closes too early and one progressbar isn't totally ended. If I go with the thread checking way, then progressbars stop in the middle of process and that's all.
Maybe you have done it and tell the Delphi n00b, how is the best way to do it.
Thanks in advance...
For this simple thing, you can use the thread OnTerminate event (which runs in the context of the main thread) just to decrement a "thread count" variable initialized to 3 at thread creation moment.
When the thread count reaches 0, you can safely close the form.
begin
//..thread creation, stuff
FThreadCount := 3;
DownloadThread.OnTerminate := DecThreadCount;
ParseThread.OnTerminate := DecThreadCount;
InsertThread.OnTerminate := DecThreadCount;
//resume threads and other stuff
end;
procedure TForm1.DecThreadCount(Sender: TObject);
begin
Dec(FThreadCount);
if FThreadCount = 0 then
Close;
end;
Are you using Windows Vista or Windows 7? Microsoft changed the way progress bars work in Vista, so that instead of immediately jumping to the indicated position, it gradually slides towards it. This means that your progress can actually be finished, but the bar won't indicate that for another second or so, so it looks like the dialog is closed before you're done, especially if the bar has a small number of progress steps.
It's kinda ugly, but you can work around this by using a helper function that does something like this:
procedure UpdateProgressBar(bar: TProgressBar);
begin
bar.StepIt;
bar.Max := bar.Max + 1;
bar.Max := bar.Max - 1;
end;
This will ensure that it immediately jumps to the correct position.
EDIT: Details in How do I make TProgressBar stop lagging?
I'd get your threads to post a message back to the dialog when they complete. Once all three messages have been received you can close the dialog.
is it possible to execute a crystal report (TCrpe component) from a Delphi non VCL main thread when Output = toWindow?
I execute reports from a background thread, and when Output is toPrinter or toExport, everything is fine.
I know that creating forms in a
Delphi non VCL main thread generally is a
bad idea.
When a Crystal
Report is executed, and Output=toWindow, the component creates the output
window on its own. So I cannot prevent that the window is created by the background thread.
So is there a clean way to
execute the report in a background
thread, and display the result in a
cleanly created form?
Version: Crystal11VCL7
The following code does not work:
procedure TMyThread.Execute;
var
cr: TCrpe;
begin
inherited;
cr:= TCrpe.Create(nil);
cr.ReportName:= 'C:\TestReport.rpt';
cr.Output:= toWindow;
cr.WindowParent:= Form1; //This is the main form
cr.Execute;
end;
It seems like the report will be created and immediately destroyed afterwards.
When I enter a message loop right after cr.Execute (while true do Application.ProcessMessages; - which is obviously a very bad idea), the report window is shown nicely.
Any idea, how to do it right? Or is it simply not possible? Thanks!
I haven't had any experience with Crystal for many years (thank goodness) but in the absence of any other reply I would consider approaching the problem from a different angle.
What I do is let the report form be created in the main thread - because, as you've said, it's not a good idea to do the VCL stuff in the background - but I generate all report /data/ in a background thread. Once the data is loaded then the thread signals the main form (or whatever) via a windows message and the report goes and connects up to the dataset/datasource.
Initially I display a blank label over the client area of the report window with a message saying something like 'Report loading...' then once the message is received it hides the label and attaches the data. Works really well here and keeps the UI responsive.