Craig Stevensson Craig Stevensson - 9 days ago 3
C++ Question

Create a form within a new Desktop using CreateDesktop/SwitchDesktop

I need to create a system modal form for an utility that should block the entire windows until certain values are entered. So I'm experimenting with creating desktops and switching. So far, creating a desktop switching to it, and going back works fine for me.

But, when I try to create a form, from within a new thread, the form does not show up but the application keeps in the newly created blank desktop, therefore blocking the screen forever until I logoff.

I made it based in the code found here:

http://developex.com/blog/system-modal-back/

// ScreenLocker.h

#pragma once

using namespace System;
using namespace System::Windows::Forms;

namespace Developex
{
public ref class ScreenLocker
{
private:
String ^_desktopName;
Form ^_form;
void DialogThread(void);

public:
static void ShowSystemModalDialog (String ^desktopName, Form ^form);
};
}


// ScreenLocker.cpp

#include "stdafx.h"
#include "ScreenLocker.h"

using namespace System::Threading;
using namespace System::Runtime::InteropServices;

namespace Developex
{
void ScreenLocker::DialogThread()
{
// Save the handle to the current desktop
HDESK hDeskOld = GetThreadDesktop(GetCurrentThreadId());

// Create a new desktop
IntPtr ptr = Marshal::StringToHGlobalUni(_desktopName);
HDESK hDesk = CreateDesktop((LPCWSTR)ptr.ToPointer(),
NULL, NULL, 0, GENERIC_ALL, NULL);
Marshal::FreeHGlobal(ptr);

// Switch to the new deskop
SwitchDesktop(hDesk);

// Assign new desktop to the current thread
SetThreadDesktop(hDesk);

// Run the dialog
Application::Run(_form);

// Switch back to the initial desktop
SwitchDesktop(hDeskOld);
CloseDesktop(hDesk);
}

void ScreenLocker::ShowSystemModalDialog(String ^desktopName, Form ^form)
{
// Create and init ScreenLocker instance
ScreenLocker ^locker = gcnew ScreenLocker();
locker->_desktopName = desktopName;
locker->_form = form;

// Create a new thread for the dialog
(gcnew Thread(gcnew ThreadStart(locker,
&Developex::ScreenLocker::DialogThread)))->Start();
}
}


Well, now I'm trying to "translate" that to Delphi and so far this is what I have:

unit Utils;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ADODB, Grids, DBGrids, ExtCtrls, ComCtrls, SyncObjs, ShellApi,
AddTimeU;

type
TFormShowThread = class(TThread)
HDesktopglobal: HDESK;
hDeskOld: HDESK;

UHeapSize: ULong;
tempDword: DWORD;

frm : TfrmBlockScreen;
private
protected
procedure Execute; override;
public
constructor Create(form : TfrmBlockScreen);
destructor Destroy; override;
end;

implementation

constructor TFormShowThread.Create(form : TfrmBlockScreen);
begin
FreeOnTerminate := True;
inherited Create(True);
frm := form;
end;


destructor TFormShowThread.Destroy;
begin
inherited;
end;

procedure TFormShowThread.Execute;
begin
hDeskOld := GetThreadDesktop(GetCurrentThreadId());

HDesktopglobal := CreateDesktop('Z', nil, nil, 0, GENERIC_ALL, nil);

SwitchDesktop(HDesktopglobal);
SetThreadDesktop(HDesktopglobal);

// tried this
Application.CreateForm(TfrmBlockScreen, frm);

// also tried this with same result
//frm := TfrmBlockScreen.Create(nil);
//frm.Show();

SwitchDesktop(hDeskOld);

CloseDesktop(HDesktopglobal);
end;

end.


I'm running that with this code:

var
frmBlockScreen : TfrmBlockScreen;
frmShowThread : TFormShowThread;
begin

frmShowThread := TFormShowThread.Create(frmBlockScreen);
frmShowThread.Priority := tpNormal;
frmShowThread.OnTerminate := ThreadDone;
frmShowThread.Start();


I don't understand why this does not work and the C++, supposedly should work, it creates a new form within the same application.

This is how I ended doing it:

I moved the form that I wanted to show, to a new project and compiled it as timeup.exe.
I created a process with the procedure shown below, sending the Desktop as parameter so I can assign the process to that desktop.
This way I didn't even needed to create a new thread... it's working so far.

Is there any flaw in this?

var
HDesktopglobal: HDESK;
hDeskOld: HDESK;

sDesktopName : String;
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;

try
hDeskOld := GetThreadDesktop(GetCurrentThreadId());

sDesktopName := 'TimeUpDesktop';

HDesktopglobal := CreateDesktop(PWideChar(sDesktopName), nil, nil, 0, GENERIC_ALL, nil);

SwitchDesktop(HDesktopglobal);
SetThreadDesktop(HDesktopglobal);

ExecNewProcess('TimeUp.exe', sDesktopName);

SwitchDesktop(hDeskOld);
CloseDesktop(HDesktopglobal);

finally
SwitchDesktop(hDeskOld);
CloseDesktop(HDesktopglobal);
end;

Application.Run;
end.



procedure ExecNewProcess(ProgramName : String; Desktop : String);
var
StartInfo : TStartupInfo;
ProcInfo : TProcessInformation;
CreateOK : Boolean;
begin

{ fill with known state }
FillChar(StartInfo,SizeOf(TStartupInfo),#0);
FillChar(ProcInfo,SizeOf(TProcessInformation),#0);
StartInfo.cb := SizeOf(TStartupInfo);
StartInfo.lpDesktop := PChar(Desktop);

CreateOK := CreateProcess(PChar(ProgramName),nil, nil, nil,False,
CREATE_NEW_PROCESS_GROUP+NORMAL_PRIORITY_CLASS,
nil, nil, StartInfo, ProcInfo);

{ check to see if successful }
if CreateOK then
//may or may not be needed. Usually wait for child processes
WaitForSingleObject(ProcInfo.hProcess, INFINITE);
end;

Answer

Before you go any further you need to recognise that the VCL design forces all VCL forms to be associated with the main GUI thread. You cannot create them on a different thread. So your design is fundamentally flawed in that way. You are never going to be able to create VCL forms in any thread other than the main GUI thread.

Even if that was not the case, your code could not do anything useful. That's because your thread does not contain a message loop. No sooner has the form been created, the thread which it is associated with terminates.

You could make this work with raw Win32 calls to CreateWindow etc. But you'd need at the very least to run a message loop in your thread for the lifetime of any windows created there.

As to why your code never switches back to the original desktop, I cannot be sure. Perhaps there is an exception in the code that attempts to create the form and so the code that restores the original desktop never runs. That code should be protected by a try/finally.

As a general point, in order to debug code which calls raw Win32 APIs, you must include error checking. You don't do any, and so you don't know which API call is failing. That would be the first step to debugging a problem like this, if we didn't already know that the approach is doomed to failure no matter what.


Perhaps I'm missing something, but it's not obvious to me why you are trying to run this form out of a different thread. Is there any reason why it cannot run out of the main GUI thread?

And to answer my own question, I am missing something. From the documentation of SetThreadDesktop:

The SetThreadDesktop function will fail if the calling thread has any windows or hooks on its current desktop (unless the hDesktop parameter is a handle to the current desktop).

Comments