Link to home
Start Free TrialLog in
Avatar of SJohnson
SJohnsonFlag for Australia

asked on

A Threaded Question... Where's my needle.

Hi Folks,

I've been programming for a few years now, and I am starting to think about threads.  I know I am really slow in taking this up, but up until today, I have really had no use of them, which means I haven't done much reading on them.

This question is in two parts.  The first part is, can anyone tell me, if I have an app which is fairly CPU intensive (ie, losts of DB calls), can I use a thread to write back to a label with an expected completion time, and actually get that label to update - even while the main app is still churning away?

The other part is, can someone give me a simple example of how I would do this?  I've played around for the last couple of days and managed to get one going (just adding the time to the caption bar of the main form), but when I start doing something intensive on the app side, the thread stops working until the app stops working.

Thanks for any help/info.

Stu.

(PS.  I'll split the points in half - 150 each - for each part if answered by two seperate people)
Avatar of Epsylon
Epsylon

Check out the C:\Program Files\Borland\Delphi5\Demos\Threads demo. It uses 'Synchronize' to draw on the form. Synchronize passes some actions to the main VCL thread, which does the actual drawing.
With a label, it will be somthing like this:


TMyThread = class(TThread)
  ...
  FLabel1Caption: String;
  procedure ChangeLabel1;
  procedure Execute;
  ....
end;


procedure TMyThread.ChangeLabel1;
begin
  Label1.Caption := FLabelCaption;
end;

procedure TMyThread.Execute;
begin
  ...
  FLabel1Caption := 'whatever';
  Synchronize(ChangeLabel1);
  ...
end;
Avatar of SJohnson

ASKER

So do I need to call Syncronize(ProcName) instead of just ProcName?

What I was doing was this (using your demo)

procedure TMyThread.Execute;
begin
  while not StopThread do
    ChangeLabel1;
end;

I should be doing:

procedure TMyThread.Execute;
begin
  while not StopThread do
    Syncronize(ChangeLabel1);
end;

Stu.
Synchronize is a bad hack which moves the execution of the "synchronized" function back into the context of the main thread. That means, your ChangeLabel1 function is only called in the main thread. And that means, if the main thread is frozen or busy, so is your other thread.

The solution is to NOT use Synchronize. However, a lot of VCL doesn't like to get touched from other threads, the VCL simply is not thread safe (shame on Borland, even Windows' standard controls are thread safe). So I am usually avoiding using Synchronize and use pure win32 APIs to update visible controls from within threads.

Well, the other things is: Some VCL components do not have a problem with being updated from a thread, so you might try updating the label directly from within the thread, but keep in mind that this is dangerous somehow.

Regards, Madshi.
Yes, the Synchronize method is a mechanism to prevent multiple threads to change controls on the form by passing it to the main thread.
Avatar of Lukasz Zielinski
I agree with comments above just one tip when doing long operation that takes lots of CPU time try to add Application.ProcessMessages, and about synchronization: use objects from OS kernel like semaphore, mutex, etc. Take a look at syncobjs.pas which ships with Delphi or post Your e-mail address I'll send You My object for threads synchronization.
ziolko.
OK.  So if I dont use Syncronize, what do I use? :)

I thought using Application.ProcessMessages in a thread would take away the whole meaning behind the thread.  Dunno, just my thoughts.

I'll check out the code in the pas files suggested above.  Cheers!


Stu.
Code for You (my little synchro object):
TSynchroReturn = (srInside, srTimedOut, srFailed, srUnknown);

     TSynchro = class(TObject)
        private
          FTimeOut: Integer;
          FHandleSemaphore: THandle;
          FEntering: Boolean;
          FName: PChar;
          procedure SetTimeOut(Value: Integer);
        public
          constructor Create(AName: string);
          constructor CreateSignaled(AName: string);
          constructor CreateCounted(AName: string;ICount: Integer);
          destructor Destroy;override;
          procedure Enter;overload;
          function Enter(WaitTime: Integer):TSynchroReturn;overload;
          procedure Leave;
          property TimeOut: Integer read FTimeOut write SetTimeOut;
        end;

implementation

procedure TSynchro.SetTimeOut(Value: Integer);
begin
  if (Value <> FTimeOut) and (not FEntering) then
    if (Value < 0) and (Value <> -1) then
      FTimeOut:=0
    else
      FTimeOut:=Value;
end;

constructor TSynchro.Create(AName: string);
begin
  FEntering:=False;
  TimeOut:=-1;
  if AName <> '' then
    FName:=PChar(AName)
  else
    FName:=SEM_NAME;
  FHandleSemaphore:=CreateSemaphore(nil,1,1,FName);
  if GetLastError = ERROR_ALREADY_EXISTS then
    begin
      // handle previouse semaphore creation
    end;
end;

constructor TSynchro.CreateSignaled(AName: string);
begin
  FEntering:=False;
  TimeOut:=-1;
  if AName <> '' then
    FName:=PChar(AName)
  else
    FName:=SEM_NAME;
  FHandleSemaphore:=CreateSemaphore(nil,0,1,FName);
  if GetLastError = ERROR_ALREADY_EXISTS then
    begin
      // handle previouse semaphore creation    
    end;
end;

constructor TSynchro.CreateCounted(AName: string;ICount: Integer);
begin
  FEntering:=False;
  TimeOut:=-1;
  if AName <> '' then
    FName:=PChar(AName)
  else
    FName:=SEM_NAME;
  if ICount > 0 then
    FHandleSemaphore:=CreateSemaphore(nil,ICount,ICount,FName)
  else
    FHandleSemaphore:=CreateSemaphore(nil,1,1,SEM_NAME);
  if GetLastError = ERROR_ALREADY_EXISTS then
    begin
      // handle previouse semaphore creation
    end;
end;

destructor TSynchro.Destroy;
begin
  if FHandleSemaphore <> 0 then
    CloseHandle(FHandleSemaphore);
  inherited Destroy;
end;

procedure TSynchro.Enter;
begin
  FEntering:=True;
  if FHandleSemaphore <> 0 then
    case TimeOut of
      -1: WaitForSingleObject(FHandleSemaphore,INFINITE);
       0: WaitForSingleObject(FHandleSemaphore,0);
    else
      WaitForSingleObject(FHandleSemaphore,TimeOut);
    end;
  FEntering:=False;
end;

function TSynchro.Enter(WaitTime: Integer):TSynchroReturn;
var ret: LongWord;
begin
  ret:=$10000000; //not used value
  FEntering:=True;
  if (WaitTime < 0) and (WaitTime <> -1) then
    WaitTime:=0;
  if FHandleSemaphore <> 0 then
    case WaitTime of
      -1: ret:=WaitForSingleObject(FHandleSemaphore,INFINITE);
       0: ret:=WaitForSingleObject(FHandleSemaphore,0);
    else
      ret:=WaitForSingleObject(FHandleSemaphore,WaitTime);
    end;
  case ret of
    WAIT_FAILED: Result:=srFailed;
    WAIT_OBJECT_0: Result:=srInside;
    WAIT_TIMEOUT: Result:=srTimedOut;
  else
    Result:=srUnknown;
  end;
  FEntering:=False;
end;

procedure TSynchro.Leave;
begin
  if FHandleSemaphore <> 0 then
    ReleaseSemaphore(FHandleSemaphore,1,nil);
end;
ziolko.
Look a lot like TCriticalSection, except the timeout...
First I've created object then found synobls.pas
:-))))
ziolko.
A good introduction to many aspects of threads in general and in combination with Delphi can be found here:

http://www.pergolesi.demon.co.uk/prog/threads/ToC.html
Hi SJohnson,

another way is to show all dynamic information in main VCL thread and don't care about non-threadsafe components. But all processes that can froze application should be started in threads. I did it for database calls, and it works fine for me. One of advantage of this way is that you can "drop " very long DB operations.

------
Igor.
For me calling the message pump (Application.ProcessMessages) from another thread is wonderful way to crush your application :)
The way of putting the lenghty operations in different than main VCL thread is very good. But you can do the other way - manualy paint the label in the other thread. I could write some example tomorrow, if nobody wants to write it that night
Until now I'm bye :)

Cheers, Frodo
Frodo > it's misunderstanding Application.ProcessMessages was my tip for long operations, for threads there is something else
ziolko.
Frodo > it's misunderstanding Application.ProcessMessages was my tip for long operations, for threads there is something else
ziolko.
Sorry, ziolko, for the missunderstanding.
SJohnson, create a form with a panel and 3 buttons, all with default names. Button1 starts the painting, Button2 stops it. Button3 puts the main thread in "sleep". And here is the code:

type
  TActionThread = class(TThread)
  public
    PanelText : TPanel;
      // shows the panels
    procedure ShowPanels;
      // hides the panels
    procedure HidePanels;
      // constructor
    constructor Create(AOwner: TWinControl);
      // destructor
    destructor Destroy; override;
      // painting
    procedure PaintMy(szText : String);
      // main thread routine
    procedure Execute; override;
  end;

  TMainForm = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Panel1: TPanel;
    Button3: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
  private
  public
    ActionThread: TActionThread;
  end;

procedure StartAction;
procedure StopAction;

var
  MainForm: TMainForm;

implementation

{$R *.DFM}

procedure TActionThread.ShowPanels;
begin
  PanelText.Visible := TRUE;
end;

procedure TActionThread.HidePanels;
begin
  PanelText.Visible := FALSE;
end;

constructor TActionThread.Create(AOwner: TWinControl);
begin
  inherited Create(TRUE);
  //create the panel for writing
  PanelText := TPanel.Create(AOwner);
  PanelText.Visible := FALSE;
  PanelText.Parent := AOwner;
  PanelText.Left := AOwner.Left + 4;
  PanelText.Top := 4;
  PanelText.Height := AOwner.Height - 8;
  PanelText.Width := AOwner.Width - 8;
  PanelText.BevelInner := bvNone;
  PanelText.BevelOuter := bvNone;
  PanelText.BringToFront;
  SendMessage(AOwner.Handle, WM_PAINT, 0, 0);
end;

destructor TActionThread.Destroy;
begin
  // free the panels we used
  PanelText.Free;
  inherited Destroy;
end;


procedure TActionThread.PaintMy(szText : String);
var DC: HDC;
  BM: hBitmap;
  DummyText : PChar;
  nLen : Integer;
begin
  if szText = '' then
    exit;
  nLen := Length(szText);
  DummyText := PChar(szText);
  // writing the text:
  DC := GetDC(PanelText.Handle);
  BM := CreateCompatibleBitmap(DC, PanelText.Width, PanelText.Height);
  SelectObject(DC, BM);
  TextOut(DC, PanelText.Left, PanelText.Top, DummyText, nLen);
  DeleteObject(BM);
  ReleaseDC(PanelText.Handle, DC);
end;


procedure TActionThread.Execute;
var
  i: integer;
begin
  i:= 0;
  while TRUE do
  begin
    Sleep(200);
    // if we need to stop the thread
    if (Terminated) then
    begin
      HidePanels;
      exit;
    end;
    inc(i);
    PaintMy(IntToStr(i));
  end;
end;

procedure StartAction;
begin
  if (MainForm.ActionThread = nil) then
  begin
    MainForm.ActionThread := TActionThread.Create(MainForm.Panel1);
    MainForm.ActionThread.Priority := tpNormal;
    MainForm.ActionThread.ShowPanels;
    SendMessage(MainForm.ActionThread.PanelText.Handle, WM_PAINT, 0, 0);
    MainForm.ActionThread.Resume;
  end;
end;

procedure StopAction;
begin
  if (MainForm.ActionThread <> nil) then
  begin
    MainForm.ActionThread.Terminate;
    MainForm.ActionThread.WaitFor;
    MainForm.ActionThread.Free;
    MainForm.ActionThread := nil;
    SendMessage(MainForm.Panel1.Handle, WM_PAINT, 0, 0);
  end;
end;

procedure TMainForm.Button1Click(Sender: TObject);
begin
  StartAction;
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  ActionThread := nil;
end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
  StopAction;
end;

procedure TMainForm.Button2Click(Sender: TObject);
begin
  StopAction;
end;

procedure TMainForm.Button3Click(Sender: TObject);
begin
  sleep(10000);
end;

end.
Maybe
 ActionThread := nil;
Should be in "initialization" part, not in FormCreate. But it works :)
Sorry guys, I'm back again.  I've been away from work for three days and I'm just going through all my email now.

Cheers,

Stu.
Epsylon: Re: Comment #1.  I used that demo as the basis for my thread.  It was good, but I couldn't get it to update the caption when the main app was working hard.

Madshi: You comments about thread safe components was interesting, but you haven't give us anything else to work from.  If I can't use syncronize as Epsylon suggested, what would you suggest?

ziolko: Can you explain this too me?  I'm not sure if I understand it fully.

AvonWyss: Thanks for the link!  I'm going to start going through that as soon as I hit send here.

FrodoBeggins: Your demo worked first go.  However, I still need to find a way of being able to update a lable.  I don't want to draw on the canvas.   Can you think of anyway to get a lable to update?

Just as a final note, I'll only be using one thread in on this form to update just one label.  I don't know if this will make a difference at all?

Thanks very much for your help so far.

Stu.
SJohnson > No matter if You want to create one or hundred threads You have to synchronize. Put code above into new unit then use it like:
s:=TSynchro.Create(//unique semaphore name);
.
.
.
s.Enter;
try
  //code that must be done exclusivly with other threads
finnaly
  s.Leave
end;
.
.
.
Method Enter is overloaded that means You can use it as procedure, in this situation thread will be suspended until no other thread will reside between Enter and Leave methods. Other way is use it like function with one param telling how long wait (in miliseconds; 0-INFITE).
Result tells You:
srInside - succesfuuly entered "exclusive" section
srTimedOut - thread was unable to enter section during specified time
srFailed - thread was unable to enter section, most probably when trying enter semaphore that dont exist
srUnknown - well I don't have explain this :-))
ziolko.
Hi Stu,

as I said I recommend using pure win32 APIs/components in such a case. TLabel is a TGraphicControl, so it is a Delphi style component, no win32 component. That's bad for threads. I would suggest removing the TLabel and instead create a win32 component with pure APIs:

  labelHandle := CreateWindow('Static', 'LabelCaption', WS_VISIBLE or WS_CHILD or SS_LEFT,
                              xPos, yPos, width, height, parentWindowHandle, 0, HInstance, nil);

You can now use SetWindowText easily to update the control from any thread without any danger.

Regards, Madshi.
Madshi, will this label get updated when the main thread is busy?
Hmmm... Good point...   :-)   To be honest: I don't know. Probably not. But I'm not sure.
I have just tried it:

- used CreateWindow to create a label
- created a thread which is passed the hwnd of the label
- used SendMessage to send a WM_SETTEXT in a thread
- 2 buttons, one for starting the thread and one for keeping the main vcl thread busy for 3 seconds.

When the thread starts (Button1), it updates the label (using SendMessage) every second. When I press Button2, the update stops. After 3 seconds it continues. BUT the thread did not continue while the main thread was busy. This is logical because SendMessage waits until it gets a response.
When you look at the implementation of Synchronize, you will see it uses SendMessage too, so what's the difference..?

I also tried PostMessage without success.
Epsylon, but with PostMessage, the thread should have been still running even if the label was not updated. Correct?
PostMessage doesn't wait for a reply, so the thread will continue.

The reason that PostMessage doesn't work must have soething to de with the fact that I'm sending an array pointer to the main thread. However, I never free the buffer.

I think Madshi knows more about SendMessage/PostMessage...


This is the Thread code:


unit Unit2;

interface

uses
  Classes, Windows, Messages, SysUtils;

type
  TMyThread = class(TThread)
  public
    constructor Create(h: HWND);
  private
    { Private declarations }
    counter: Integer;
    FHandle: HWND;
    a: array[0..255] of Char;
  protected
    procedure Execute; override;
  end;

implementation

constructor TMyThread.Create(h: HWND);
begin
  FHandle := h;
  inherited Create(false);
end;

procedure TMyThread.Execute;
begin
  while not Terminated do
  begin
    Sleep(1000);
    Inc(counter);
    StrPCopy(a, IntToStr(counter));
    SendMessage(FHandle, WM_SETTEXT, 0, Integer(@a));
  end;
end;

end.
Hi guys...

>> When you look at the implementation of Synchronize, you will see it uses SendMessage too, so what's the difference..?

I have to admit: In this case there's not much of a difference... In the new version (not released yet) of my exception handling package I'm creating a whole window with buttons with bitmaps and more with pure win32 APIs in a thread. This would probably be an option here. If the whole window belongs to your secondary thread, you are totally independent of the main thread. But it's a lot of work and pain just for updating a label. Perhaps it would be simpler to just add some Application.ProcessMessages to the main thread and then either use Synchronize or SetWindowText from the secondary thread.

>> The reason that PostMessage doesn't work must have soething to de with the fact that I'm sending an
array pointer to the main thread. However, I never free the buffer.

It's a static variable of your TThread class, so the pointer you send is always the same, but the characters are changed all the time. So in the moment where the main thread is alive again, it gets e.g. 3 WM_SETTEXT messages, all with the same pointer, thus all containing only the newest text/counter.
Or does PostMessage with WM_SETTEXT not work at all? Hmmm... In that case I would guess that maybe WM_SETTEXT only works with SendMessage?

Regards, Madshi.
PostMessage does not work at all...
Hi again all.  This is turning into a really interesting read!

I really like Madshi's idea of creating the label through APIs.  

With regards to FrodoBeggins comment.  If I use the code he supplied and create a component out of it (still drawing directly on the canvas), will this cause a problem?  

And finally, I guess this is something I should have asked originally but I only just thought of it.  What I am trying to do is write something which should how much time is left in an operation.  What I am doing presently is reading the position of a TProgressBar which tells me how far through we are through an operation.  This works pretty well, so what I want to do is still read a TProgressBars MAX and POSITION properties.

Can anyone think of any issues this may cause?

Thank you all so much for all your help.  Even if I don't manage to get this going, I've learnt a great deal already and I really appreciate it!

Thanks again,

Stu.
Hi, Folks

Right now I'm on a vacation at the black sea and I rarely look at the net :)

But now I found some time (durring my bussy day - you know: sleepping late, beaching, eating...)

It is getting a realy good discussion. Unfortunately my brain is so liquid right now, so I can't get all of it. Buttt.... 'bout my code.
I realy don't care if it is TLabel ot TPanel. The only reason for me choosing TPanel is that it is TWinControl and he can own the TextPanel I create. But the panel can be owned by the Form itself too, and placed over the label.

The panel I create has a "Caption" property, which can't be used if the main thread is asleep (it is updated by the VCL). But in fact I use only it's Canvas through it's Handle. That seems pretty thread-safe.

And, as Max Planck says, the best proof of an idea is it's testing (or something similar). Well, the text gets updated during the main thread's sleeping. So - i think it works.

AND about the TProgressBar. I wonder WHO updates the POSITION value. If it is the main thread... You know, it sounds not good :))

Rgds,
Frodo
Hi all

I'm back online. :)

Now I *think* I see what wrong with Epsylon's threading.
The window you are trying to update (called HWND, as usual :)) is controlled by your main VCL thread. So you send him the message WM_SETTEXT, but he can't catch it until the main thread is sleeping. It gets the 3 messages after it is awake and, in fact, sets the text 3 times (very fast). I'm not sure if it is the last text only (as said by Madshi) ot the 3 different numberstrings (?). Windows handles the WM_SETTEXT message somehow differently than all other messages. But that's not the point.
Every time you create a thread it is created (by Windows) as worker thread. That's easier, and the THREADINFO structure is smaller. It becomes GUI thread after the first GUI API call produced by the thread. But your thread does nothing with the GUI. So, in fact, you may send WM_SETTEXT messages from 100 threads, but the main VCL thread (which is asleep) is responsible for handling them. And it handles them after it awakes.

Rgds,
Frodo
Madshi: About the CreateWindow - I don't create the window (panel) in the secondary thread, I just use it's handle there. So, in fact, all my VCL calls are in the main thread. (In fact, I think that a window can be created in it's own thread by CreateMDIWindow(...). Not sure.)
About WM_SETTEXT - sorry, it is specialy processed only if you send it to a thread in other process. As you write, the last string is displayed 3 times.

Rgds,
Frodo
Yeah right, my idea works only if the parent window(s) is/are also created in the secondary thread, which is quite a bit of work.
So here comes another version. The constructor of the thread wants any TControl (in the exaple I use TLabel) to create a panel over it. To start, create a form called MainForm, 3 buttons and a label. The main thread routine writes numbers on the panel (started by Button1Click, stopped by Button2Click). It works even if the main VCL thread is asleep (which can be done by Button3Click).
Don't forget to change the AutoSize property of the label to False before you use it

Rgds,
Frodo

unit thPaintMain;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  ComCtrls, extctrls, StdCtrls;

type
  TActionThread = class(TThread)
  public
    PanelText : TPanel;
      // shows the panels
    procedure ShowPanels;
      // hides the panels
    procedure HidePanels;
      // constructor
    constructor Create(AOwner: TControl);
      // destructor
    destructor Destroy; override;
      // painting
    procedure PaintMy(szText : String);
      // main thread routine
    procedure Execute; override;
  end;

  TMainForm = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    Label1: TLabel;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
  public
  end;

procedure StartAction;
procedure StopAction;

var
  MainForm: TMainForm;
  ActionThread: TActionThread;

implementation

{$R *.DFM}

procedure TActionThread.ShowPanels;
begin
  PanelText.Visible := TRUE;
end;

procedure TActionThread.HidePanels;
begin
  PanelText.Visible := FALSE;
end;

constructor TActionThread.Create(AOwner: TControl);
begin
  inherited Create(TRUE);
  //create the panel for writing
  PanelText := TPanel.Create(AOwner.Parent);
  PanelText.Visible := FALSE;
  PanelText.Parent := AOwner.Parent;
  PanelText.Left := AOwner.Left;
  PanelText.Top := AOwner.Top;
  PanelText.Height := AOwner.Height;
  PanelText.Width := AOwner.Width;
  PanelText.BevelInner := bvNone;
  PanelText.BevelOuter := bvNone;
  PanelText.BringToFront;
  SendMessage(AOwner.Parent.Handle, WM_PAINT, 0, 0);
end;

destructor TActionThread.Destroy;
begin
  // free the panels we used
  PanelText.Free;
  inherited Destroy;
end;


procedure TActionThread.PaintMy(szText : String);
var DC: HDC;
  BM: hBitmap;
  DummyText : PChar;
  nLen : Integer;
begin
  if szText = '' then
    exit;
  nLen := Length(szText);
  DummyText := PChar(szText);
  // writing the text:
  DC := GetDC(PanelText.Handle);
  BM := CreateCompatibleBitmap(DC, PanelText.Width, PanelText.Height);
  SelectObject(DC, BM);
  TextOut(DC, 1, 1, DummyText, nLen);
  DeleteObject(BM);
  ReleaseDC(PanelText.Handle, DC);
end;


procedure TActionThread.Execute;
var
  i: integer;
begin
  i:= 0;
  while TRUE do
  begin
    Sleep(200);
    // if we need to exit the thread
    if (Terminated) then
    begin
      HidePanels;
      exit;
    end;
    inc(i);
    PaintMy(IntToStr(i));
  end;
end;

procedure StartAction;
begin
  if (ActionThread = nil) then
  begin
    ActionThread := TActionThread.Create(MainForm.Label1);
    ActionThread.Priority := tpNormal;
    ActionThread.ShowPanels;
    SendMessage(ActionThread.PanelText.Handle, WM_PAINT, 0, 0);
    ActionThread.Resume;
  end;
end;

procedure StopAction;
begin
  if (ActionThread <> nil) then
  begin
    ActionThread.Terminate;
    ActionThread.WaitFor;
    ActionThread.Free;
    ActionThread := nil;
    SendMessage(MainForm.Handle, WM_PAINT, 0, 0);
  end;
end;

procedure TMainForm.Button1Click(Sender: TObject);
begin
  StartAction;
end;

procedure TMainForm.Button2Click(Sender: TObject);
begin
  StopAction;
end;

procedure TMainForm.Button3Click(Sender: TObject);
begin
  sleep(10000);
end;

procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  StopAction;
end;

initialization
  ActionThread := nil;

end.
SJohnson: "I don't want to draw on the canvas." Well, that's whe way to show something in Windows. If you dig in the VCL you can find that changing the text of a label in fact does the folowing API call:

unit stdctrls;
....
procedure TCustomLabel.DoDrawText(var Rect: TRect; Flags: Longint);
.....
DrawText(Canvas.Handle, PChar(Text), Length(Text), Rect, Flags);

But we don't want to use the VCL (it is not thread-safe). So I manualy draw on the canvas, with
  TextOut(DC, 1, 1, DummyText, nLen);

That's what is the Canvas made for - drawing :-)

Rgds,
Frodo
Hi Guys,

The people in CS have terminated my account because I use two different ones - one for my work related stuff (which is the SJohnson one) and one for my non-work related stuff (this one).

I've asked them what I can do about open questions on my SJohnson account and I guess I'll just have to wait to hear back from them.

Personally, I think it's crap that I can't have two accounts - considering I was told 3 years ago I could.  But that's life I guess.

So, if I can't get this account reactivated, I'll post another question with the same title.

But I'll add a comment now for you FrodoBeggins..

I didn't actually realise this.  OK.  I accept your comment as an answer because it worked really well.  I guess I was just being lazy and didn't want to have to setup all the properties manually.  But if that is the way I should do it, I'd prefer to do it right the first time.

Thank you to everyone who has helped me with this.  I appreciate it.

If I dont hear anything back from CS today, I'll open a new question and post the link here.

Thanks again for your help.

Stu.
ASKER CERTIFIED SOLUTION
Avatar of FrodoBeggins
FrodoBeggins

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Hopefully you've already been helped with this question, but thought you'd appreciate knowing this.  It would be great if you could bring this question to a conclusion, awarding the experts above who helped you with points or a comment to them on your status today.

WindowsUpdate has new updates for .NET users; Details follow - Microsoft .NET Framework
The .NET Framework is a new feature of Windows. Applications built using the .NET Framework are more reliable and secure. You need to install the .NET Framework only if you have software that requires it.

For more information about the .NET Framework, see http://www.microsoft.com/net. (This site is in English.)

System Requirements
The .NET Framework can be installed on the following operating systems:
Windows 98
Windows 98 Second Edition (SE)
Windows Millennium Edition (Windows Me)
Windows NT 4.0® (Workstation or Server) with Service Pack 6.0a
Windows 2000 with the latest service pack installed (Professional, Server, Datacenter Server, or Advanced Server)
Windows XP (Home Edition and Professional)
You must be running Internet Explorer version 5.01 or later for all installations of the .NET Framework.

To install the .NET Framework, your computer must meet or exceed the following software and hardware requirements:

Software requirements for server operating systems:
MDAC 2.6
Hardware requirements:
For computers running only a .NET Framework application, Pentium 90 mHz CPU with 32 MB memory or the minimum CPU and RAM required by the operating system, whichever is higher.
For server operating systems, Pentium 133 mHz CPU with 128 MB memory or the minimum CPU and RAM required by the operating system, whichever is higher.
Recomended software:
MDAC 2.7 is recommended.
Recommended hardware: For computers running only a .NET Framework application, Pentium 90 MHz CPU with 96 MB memory or the minimum CPU and RAM required by the operating system, whichever is higher.
For server operating systems, Pentium 133 MHz CPU with 256 MB memory or the minimum CPU and RAM required by the operating system, whichever is higher.

How to use -> Restart your computer to complete the installation. No other action is required to run .NET Framework applications. If you are developing applications using the .NET Framework, you can use the command-line compilers or you can use a development environment, such as Visual Studio .NET, that supports using the .NET Framework.

How to uninstall
To uninstall the .NET Framework: Click Start, point to Settings, and then click Control Panel (In Windows XP, click Start and then click Control Panel.).
Click Add/Remove Programs.
Click Microsoft .NET Framework (English) v1.0.3705 and then click Change/Remove.
More here  http://www.microsoft.com/net/

The .NET topic is being considered for addition to our All Topics link soon, so this may interest you as well:
https://www.experts-exchange.com/newtopics/Q.20276589.html

EXPERTS POINTS are waiting to be claimed here:  https://www.experts-exchange.com/commspt/Q.20277028.html

":0)
Asta


asta whats with the .net advertisment in every q
you working for ms?  :)


http://www.linux-mandrake.com/

:o)
Asta and all,

This account was deleted by EE CS about 6 months ago.  It was done before I had a chance to close all my opened questions (which I believe is totally unfair).  I can not award points, or close questions which are opened under this account name.  This account was deemed a second account (even though this was the account myself and another work mate use).  So, what am I to do?  I have two questions opened worth 300 points each.  The other question (https://www.experts-exchange.com/jsp/qManageQuestion.jsp?ta=delphi&qid=20142669) was never ansered, so it can be deleted.  

My SJohnson account had around 1600 expert points.  Can I have the points awarded from that account into people I nominate, as I believe a lot of people went to a lot of effort to help me out.

I appologise to all experts in this thread who spent time with me on this.  When the account was deleted, I stopped getting notifications on all my old questions.  This was the first notification I have gotten.  Most of you know that I would never deliberately not pay up for the help given (in fact, I usually award more points that what I originally allocated if someone goes beyond the call of duty).

I hope this can be cleared up soon.

Stu.

PS.  Asta, as Barry said, why the Microsoft Spam?  Don't we get enough of that without having it thrown around here?

http://www.redhat.com/ - for stability, speed and price, you can't go wrong with RedHat :)
Hi, Stu, and all.

astaec sent me this link and I have also sent it to ComTech, our Administrative Liaison to see what can be done about migrating these points.

To explain what astaec was attempting to do with these questions, was to help bring old and forgotten questions back to the foreground for a couple of reasons, and to add value in the process.   Email notifs in this timeframe weren't working, and many have returned with appreciation to close their open items as a result of these postings by her.  She used Power Search for any .net keyword results, which was the source of these postings.  The intention was to help close old items and add any value she could in the process.

Moondancer - EE Moderator
ComTech will help you with the account details, I will help you with what you'd like done here.
Would you like this awarded, or refunded and moved to the PAQ at zero points?
Hello Stu, Moondancer has alerted me to this problem, there is no comment there as to who closed the account, But she did locate the Admin that did it, and is no longer with us.  As it is a work account, I will allow it, and have reopened it, as this is not considered a duplicate account.

So you now have:
SJohnson as a work account,
and StuartJohnson as a personel account.

If you need help geting into the old account, email me with the email you want to use, and password...if forgotten.

Very best to you,
ComTech
Community Support Admin
@ Experts-Exchange

Moondancer,

I would like, if possible, to award the points to FrodoBeggins from my now deleted SJohnson account which I originally posted the question under.  If there is a problem with this, can you please let me know?

I'd appreciate you looking into the points issues as this was a real low thing to do - especially when I was assured was I was allowed to maintain two accounts simultaneously under two names (as one was used for work, one for my home use).  Again, please let me know if there is any issues at all.

Cheers,

Stu
Hi ComTech,

Thank you very much for doing this.  I appreciate it!

To FrodoBeggins, I will have to log back in under my other account so I can award you the points.  My appologies to all for the lack of follow up on my part with this question.  It was a bad oversight :)

Cheers all,

Stu
Again, sorry for the delay in cleaning this up.

All the best folks, I'm off to bed :)

Stu
btw-Stu, my email is comtech@experts-exchange.com

CT

As your account was closed, I believe you have nothing to apologize for, IMHO.

Only wish I had known sooner.  Good night.