Solved

Component to record keystrokes and playback

Posted on 2002-06-23
13
401 Views
Last Modified: 2010-04-04
I am looking for a component or code to record keyboard keys and then playing them back at the same speed. Does anyone know of any? Thanx in advance.
0
Comment
Question by:skynergy
13 Comments
 
LVL 2

Expert Comment

by:Tasomia
ID: 7101457
Hi

TRecorder is a class that allows to record and play back mouse and keyboard events. It uses a WH_JOURNALPLAYBACK Hook. TRecorder is a freeware.

http://www.cyamon.com/recorder.html


tas
0
 
LVL 33

Expert Comment

by:Slick812
ID: 7102228
if you want some code for a WH_JOURNALRECORD hook that will record the mouse and keboard events into an array and then play them back with a WH_JOURNALPLAYBACK, let me know
0
 

Author Comment

by:skynergy
ID: 7112845
Thanx for all the feedback. I would love some code to record keys only and then editing them if possible in a list. Thanx in advance.
0
 
LVL 33

Expert Comment

by:Slick812
ID: 7115423
Could you give some more info about what you need? I don't understand when you said
"and then editing them if possible in a list."
Did you look at the TRecorder, it may be more easy to use than doing all the code. Editing the key strokes is not what the WH_JOURNALRECORD operation was made for, but you could do it.
0
 
LVL 33

Expert Comment

by:Slick812
ID: 7115644
here is the code for a WH_JOURNALRECORD hook application that I made using a list box to list the mouse and keyboard events



unit ListHook1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  Buttons, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    but_Exit: TButton;
    ListBox1: TListBox;
    sbut_StartJour: TSpeedButton;
    sbut_EndJour: TSpeedButton;
    sbut_PlayJour: TSpeedButton;
    Timer1: TTimer;
    sbut_SaveJFile: TSpeedButton;
    sbut_LoadJFile: TSpeedButton;
    procedure but_ExitClick(Sender: TObject);
    procedure sbut_StartJourClick(Sender: TObject);
    procedure sbut_EndJourClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure sbut_SaveJFileClick(Sender: TObject);
    procedure sbut_LoadJFileClick(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure sbut_PlayJourClick(Sender: TObject);
  private
    { Private declarations }
    TimerStr: String;
    EventMsgArray: Array of TEVENTMSG;
    procedure DrawDesk(DisText: String; Red: Boolean);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;
  JHook: THandle;
  TempEvMsg: TEventMsg;
  Track, SysModal, GotMove, DoDelay: Boolean;
  StartTime, FirstGo: Cardinal;
  NumMsgs, PlayNum: Integer;
  WARect: TRect;

implementation

{$R *.DFM}

procedure TForm1.but_ExitClick(Sender: TObject);
begin
Close;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
NumMsgs := 0;
JHook := 0;
PlayNum := 0;
Track := False;
SysModal := False;
SetLength(EventMsgArray,1);
end;

procedure TForm1.DrawDesk(DisText: String; Red: Boolean);
var
DeskDC, hBrush: Integer;
begin
if not SystemParametersInfo(SPI_GETWORKAREA,0,@WARect,0) then
WARect := Rect(0,0,Screen.Width,Screen.Height - 32);
WARect.TopLeft := Point(WARect.Right-158,WARect.Bottom-60);
DeskDC := GetDC(0);
hBrush := 0;
if Red then
  begin
  hBrush := CreateSolidBrush($000000FF);
  SelectObject(DeskDC,hBrush);
  SetBkColor(DeskDC,$000000FF);
  end;
Rectangle(DeskDC,WARect.Left,WARect.Top,WARect.Right,WARect.Bottom);
InflateRect(WARect,-3,-2);
DrawText(DeskDC,PChar(DisText),Length(DisText),WARect, DT_LEFT or DT_WORDBREAK);
DeleteObject(hBrush);
ReleaseDC(GetDesktopWindow,DeskDC);
TimerStr := DisText;
Timer1.Tag := 0;
Timer1.Interval := 700;
Timer1.Enabled := True;
end;

function JHookProc(Code:integer; wParam: Longint; var EventStrut: TEVENTMSG): Longint; stdcall;
begin
{this is the JournalRecordProc}
Result := CallNextHookEx( JHook, Code, wParam, Longint(@EventStrut) );
if Code < 0 then Exit;

{you should cancel operation if you get HC_SYSMODALON}
if Code = HC_SYSMODALON then SysModal := True
else if Code = HC_SYSMODALOFF then SysModal := False;

if Code = HC_ACTION then
  begin
  if SysModal then Exit;
  if EventStrut.message = WM_KEYDOWN then
  begin
  if (EventStrut.paramL = 17923) {17923 is for Ctrl+Break}
  or (Chr(LOBYTE(LOWORD(EventStrut.paramL)))='Q') and (GetKeyState(VK_CONTROL) < 0) then
    Form1.sbut_EndJourClick(Form1)  {Ctrl+Q and Ctrl+Break will End the Journal record}
  end  else if (Chr(LOBYTE(LOWORD(EventStrut.paramL)))='S') and (GetKeyState(VK_CONTROL) < 0) then
    begin
    {Ctrl+S will start the journal record}
    if Track = False then
      begin
      StartTime := GetTickCount;
      {It is nessarry to subtract the TickCount to get the amount of ticks between events
      I do not use the Event Time in my playback Proc, but you might want to}
      FirstGo := 0;
      {FirstGo is used to remove the Ctrl and S key up events}
      Form1.DrawDesk('JOURNAL NOW RECORDING'#10'Press Ctrl+q to Stop', False);
      end;
    Track := True;
    end;
If Track then
    begin
    if NumMsgs < 1024 then
    {I have limited the event log to 1024 for NO reason
    except for those who forget or don't know how to stop
    the the journaling}
      begin
      if EventStrut.message = WM_MOUSEMOVE then
        begin
        {I found the 1000's of mouse move messages to be
        totally useless, so I skip all the Mouse Move messages
        except the Last one before another type of event}
        TempEvMsg := EventStrut;
        GotMove := True;
        end else
        begin
        if GotMove then
          begin
          GotMove := False;
          Form1.EventMsgArray[NumMsgs] := TempEvMsg;
          Dec(Form1.EventMsgArray[NumMsgs].time,StartTime);
          Inc(NumMsgs);
          SetLength(Form1.EventMsgArray, NumMsgs+1);
          end;
        if FirstGo < 2 then
        if (EventStrut.message = WM_KEYUP) then
          begin
          {this prevents the Ctrl and s key up from being recorded}
          if EventStrut.paramL = 7441 then Inc(FirstGo);
          {7441 is Ctrl key}
          if Chr(LOBYTE(LOWORD(EventStrut.paramL))) = 'S' then Inc(FirstGo);
          Exit
          end else FirstGo := 2;
        Form1.EventMsgArray[NumMsgs] := EventStrut;
        Dec(Form1.EventMsgArray[NumMsgs].time,StartTime);
        Inc(NumMsgs);
        SetLength(Form1.EventMsgArray, NumMsgs+1);
        end;
      end;
    end;
  end;
end;

procedure TForm1.sbut_StartJourClick(Sender: TObject);
var
Rect1: TRect;
begin
{this button click starts the Journal Record hook}
if Track then
  begin
  Messagebox(0,'Mouse tracking has already been started', 'No can Restart', MB_OK or MB_ICONQUESTION);
  Exit;
  end;

GotMove := False;
FirstGo := 0;
JHook := SetWindowsHookEx(WH_JOURNALRECORD , @JHookProc, 0{hInstance}, 0);

if JHook > 0 then
  begin
  NumMsgs := 1;
{set NumMsgs to 1 because the first EventMsgArray is used for
file verification and NOT for events}
  Track := False;
  SysModal := False;
  SetLength(EventMsgArray,2);

  {If the user has changed screen Resolution OR moved the task bar
  then the Position of mouse clicks will NOT be correct, So get the
  Work Area and record it, so you can compare it later for playback}
  if not SystemParametersInfo(SPI_GETWORKAREA,0,@Rect1,0) then
  Rect1 := Rect(0,0,Screen.Width,Screen.Height - 32);
  EventMsgArray[0].message := Rect1.Left;
  EventMsgArray[0].paramL := Rect1.Top;
  EventMsgArray[0].paramH := Rect1.Right;
  EventMsgArray[0].time := Rect1.Bottom;
  EventMsgArray[0].hwnd := 65432;
  Perform(WM_SYSCOMMAND,SC_MINIMIZE or SC_ICON,0);
  DrawDesk('JOURNAL RECORD READY'#10'Press Ctrl+s to Start', False);
  end else
  ShowMessage('Journal Hook could not be set');
end;

procedure TForm1.sbut_EndJourClick(Sender: TObject);
var
i: Integer;
begin
{This is an Emergency Stop Journaling button click}
Track := False;
if JHook < 1 then
NumMsgs := 0 else
UnhookWindowsHookEx(JHook);
Timer1.Enabled := False;
SetForegroundWindow(Handle);
Perform(WM_SYSCOMMAND,SC_RESTORE,0);
Application.ProcessMessages;
InvalidateRect(0,@WARect, True);
Sleep(200);
JHook := 0;
if NumMsgs >1 then
  begin
  ListBox1.Clear;
    {start the for loop with 1 because the 0 if for Work Area verification}
  for i := 1 to NumMsgs-1 do
    begin
    case EventMsgArray[i].message of
      WM_LBUTTONDOWN: ListBox1.Items.Add('L Down '+IntToStr(EventMsgArray[i].paramL)+'  '+IntToStr(EventMsgArray[i].paramH));
      WM_LBUTTONUP: ListBox1.Items.Add('L UP '+IntToStr(LOWORD(EventMsgArray[i].paramL))+'  '+IntToStr(HIWORD(EventMsgArray[i].paramL)));
      WM_MOUSEMOVE: ListBox1.Items.Add('Move '+IntToStr(EventMsgArray[i].paramL)+'  '+IntToStr(EventMsgArray[i].paramH));
      WM_RBUTTONDOWN: ListBox1.Items.Add('R Down '+IntToStr(EventMsgArray[i].paramL)+'  '+IntToStr(EventMsgArray[i].paramH));
      WM_RBUTTONUP: ListBox1.Items.Add('R UP '+IntToStr(EventMsgArray[i].paramL)+'  '+IntToStr(EventMsgArray[i].paramH));
      WM_KEYDOWN: ListBox1.Items.Add('Key Down '+IntToStr(LOBYTE(LOWORD(EventMsgArray[i].paramL)))+'  '+IntToStr(EventMsgArray[i].paramH));
      WM_KEYUP: ListBox1.Items.Add('Key UP '+IntToStr(LOBYTE(LOWORD(EventMsgArray[i].paramL)))+'  '+IntToStr(EventMsgArray[i].paramH));
      else ListBox1.Items.Add('Other '+IntToStr(EventMsgArray[i].message)+'  '+IntToStr(EventMsgArray[i].paramH));
      end;
    end;

      while EventMsgArray[NumMsgs-1].paramL = 7441 do
      begin
      {This removes the Ctrl key mouse down repeat messages}
      Dec(NumMsgs);
      if NumMsgs < 3 then Break;
      end;

  end else
  ShowMessage('No Journal Messages are recorded');

end;

procedure TForm1.sbut_SaveJFileClick(Sender: TObject);
var
  SaveFile: File of TEventMsg;
  i: Integer;
  FileName: String;
begin
if NumMsgs > 1 then
  begin
  FileName := 'C:\Stuff\Play Journal1.pbj';
  AssignFile(SaveFile, FileName);
  Rewrite(SaveFile);
  for i := 0 to NumMsgs-1 do
  Write(SaveFile,EventMsgArray[i]);
  CloseFile(SaveFile);
  end;

end;

procedure TForm1.sbut_LoadJFileClick(Sender: TObject);
var
  FileName: String;
  LoadFile: File of TEventMsg;
  i: Integer;
  Rect1: TRect;
begin
FileName := 'C:\Stuff\Play Journal1.pbj';
if FileExists(FileName) then
  begin
  AssignFile(LoadFile, FileName);
  Reset(LoadFile);
  NumMsgs := FileSize(LoadFile);
  if NumMsgs > 1 then
    begin
    SetLength(EventMsgArray, NumMsgs);
    for i := 0 to NumMsgs-1 do
    Read(LoadFile,EventMsgArray[i]);
    CloseFile(LoadFile);

    {this is to check the file to see if it is a valid .ppj file for this
    version AND to see if the DeskTop work Area matches the file's work area}
    if not SystemParametersInfo(SPI_GETWORKAREA,0,@Rect1,0) then
    Rect1 := Rect(0,0,Screen.Width,Screen.Height - 32);
    if EventMsgArray[0].hwnd <> 65432 then
      begin
      {the 65432 is a number just to verify that this is a valid .pbj file}
      SetLength(EventMsgArray, 0);
      ShowMessage('the file "'+FileName+'" is NOT an Event File compatible with this version. No Playback is availible');
      end else
    {check to see if the Desktop work area matches}
      if (abs(EventMsgArray[0].message) <> Rect1.Left) or
      (abs(EventMsgArray[0].paramL) <> Rect1.Top) or
      (abs(EventMsgArray[0].paramH) <> Rect1.Right) or
      (abs(EventMsgArray[0].time) <> Rect1.Bottom) then
        begin
        SetLength(EventMsgArray, 0);
        ShowMessage('the DeskTop Dimentions are NOT compatible with this Playback File. No Playback is availible');
        end else
        begin
        ListBox1.Clear;
        for i := 1 to NumMsgs-1 do
          begin
          {this adds the messages to a memo, just to check in a practice version}
            case EventMsgArray[i].message of
              WM_LBUTTONDOWN: ListBox1.Items.Add('L Down '+IntToStr(EventMsgArray[i].paramL)+'  '+IntToStr(EventMsgArray[i].paramH));
              WM_LBUTTONUP: ListBox1.Items.Add('L UP '+IntToStr(LOWORD(EventMsgArray[i].paramL))+'  '+IntToStr(HIWORD(EventMsgArray[i].paramL)));
              WM_MOUSEMOVE: ListBox1.Items.Add('Move '+IntToStr(EventMsgArray[i].paramL)+'  '+IntToStr(EventMsgArray[i].paramH));
              WM_RBUTTONDOWN: ListBox1.Items.Add('R Down '+IntToStr(EventMsgArray[i].paramL)+'  '+IntToStr(EventMsgArray[i].paramH));
              WM_RBUTTONUP: ListBox1.Items.Add('R UP '+IntToStr(EventMsgArray[i].paramL)+'  '+IntToStr(EventMsgArray[i].paramH));
              WM_KEYDOWN: ListBox1.Items.Add('Key Down '+IntToStr(LOBYTE(LOWORD(EventMsgArray[i].paramL)))+'  '+IntToStr(EventMsgArray[i].paramH));
              WM_KEYUP: ListBox1.Items.Add('Key UP '+IntToStr(EventMsgArray[i].paramL)+'  '+IntToStr(EventMsgArray[i].paramH));
              else ListBox1.Items.Add('Other '+IntToStr(EventMsgArray[i].message)+'  '+IntToStr(EventMsgArray[i].paramH));
            end;
          end;
        end;
    end;
  end;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
If Timer1.Tag = 0 then
  begin
  DrawDesk(TimerStr, True);
  Timer1.Tag := 1;
  Timer1.Interval := 300;
  end else
  begin
  DrawDesk(TimerStr, False);
  Timer1.Tag := 0;
  Timer1.Interval := 800;
  end;
end;

function PlaybackProc(Code:integer; wParam: Integer; var EventStrut: TEVENTMSG): Integer; stdcall;
begin
{this journal playback function is very difficult to understand.
Each event in the EventMsgArray is called here 2 or 3 times or more,
first with a HC_GETNEXT code to get a delay Result, if there is a Delay
Result larger than 0,then the event is NOT Fired, and a HC_GETNEXT is
called again after the delay period. If the Delay Result is 0 then the event
is fired and the HC_SKIP code is called, which advances the PlayNum to the
next EventMsgArray and then it does the HC_GETNEXT to start the loop again.}
Result := 0;

if Code < 0 then
  begin
  {if code is less than 0 pass it}
  Result := CallNextHookEx( JHook, Code, wParam, Integer(@EventStrut) );
  Exit;
  end;

if Code = HC_SKIP then
  begin
  if SysModal then Exit;
  Inc(PlayNum);
  DoDelay := True;
  {set this to true to get a Delay in the HC_GETNEXT}
  if PlayNum >= NumMsgs-1 then
    begin
    {this ends the playback}
    UnhookWindowsHookEx(JHook);
    Form1.Timer1.Enabled := False;
    Sleep(700);
    InvalidateRect(0,@WARect, True);
    SetForegroundWindow(Form1.Handle);
    Form1.Perform(WM_SYSCOMMAND,SC_RESTORE,0);
    end;
  Exit;
  end;

if Code = HC_GETNEXT then
  begin
  if SysModal then Exit;
  Move(Form1.EventMsgArray[PlayNum], EventStrut, SizeOf(TEVENTMSG));
  if DoDelay then
    begin
    Result := 200;
    {I tried lots of different combinations of setting the
    delay Result, but for playback using a constant between 200 and 500
    seemed best. This eliminates user slow typing and slow clicking}

    //Result := Form1.EventMsgArray[PlayNum].time - GetTickCount;
    {Setting the Result to time of EventStrut minus TickCount will simulate the
    event timing, more or less}
    DoDelay := False;
    end; {else
  EventStrut.time := 300{EventStrut.time+ StartTime};
  {I tried setting this time to different values, but it seems to
  have little or No effect}

  {unlike many other windows system Proc, this uses the memory value
  in the Proc parameter EventStrut (lParam), when this function returns.
  Notice that it is declared as a var. You do NOT need to to use
  CallNextHookEx, because this is not used like other Hooks, to get messages,
  this fires mouse and keyboard events outside of a "hook Chain"}
  Exit;
  end else
if Code = HC_SYSMODALON then  SysModal := True
else if Code = HC_SYSMODALOFF then
  begin
  SysModal := False;
  end;

CallNextHookEx( JHook, Code, wParam, Integer(@EventStrut));
{you don't need this CallNextHookEx here but it is standard for hook proc
the Journal hook is different is not in a hook Chain}
end;

procedure TForm1.sbut_PlayJourClick(Sender: TObject);
begin
ShowMessage('NumMsgs '+IntToStr(NumMsgs));
if NumMsgs > 0 then
  begin
  PlayNum := 1;
  DoDelay := False;
  ReleaseCapture;
  SysModal := False;
  Form1.Perform(WM_SYSCOMMAND,SC_MINIMIZE or SC_ICON,0);
  DrawDesk('JOURNAL PLAYBACK'#10'Please Wait', False);
  StartTime := GetTickCount;
  JHook := SetWindowsHookEx(WH_JOURNALPLAYBACK , @PlaybackProc, 0, 0);
  end;

end;

end.



- - - - - - - - - - - - - - - - - - - - - - - - - -

let me know what else you might need
0
 

Author Comment

by:skynergy
ID: 7124430
Hi again. With your code it keeps on giving me a message "Journal Hook could not be set". And I can't record any further. Any ideas?
0
What Security Threats Are You Missing?

Enhance your security with threat intelligence from the web. Get trending threat insights on hackers, exploits, and suspicious IP addresses delivered to your inbox with our free Cyber Daily.

 
LVL 33

Expert Comment

by:Slick812
ID: 7125522
you say
"With your code it keeps on giving me a message "Journal Hook could not be set". And I can't record any further."

I don't really understand the lack of imformation in your statement. You say "And I can't record any further." Does this mean that you got it to record and show the results in the list box?

the error message that you refer to comes from


JHook := SetWindowsHookEx(WH_JOURNALRECORD , @JHookProc, 0{hInstance}, 0);

if JHook > 0 then
  begin

  end else
  ShowMessage('Journal Hook could not be set');

If you have started a J Hook you have to end it before you can start another one. If you have not started a JHook and get this error message, I don't know what could be the problem, I have started JHooks in several different types of windows systems and many types of computers.
0
 

Author Comment

by:skynergy
ID: 7126373
Yes, JHook = 0 and it skips to the end and gives the message: 'Journal Hook could not be set'. Understanding from your answer you don't know why this happens?
0
 
LVL 33

Expert Comment

by:Slick812
ID: 7128800
I gave you the code above from an app that I made, and it works, it sets the J Hook and gets the messageses in the list box. I do not know what could be wrong? Does the TRecorder work for you?
0
 
LVL 33

Expert Comment

by:Slick812
ID: 7132178
here is a simple JHook creation to try test the Journal Hook creation you have not been able to do. Try this and see if the hJHook is greater than 0.


var
  Form1: TForm1;
  hJHook: THandle;
  Journaling: Boolean = False;

implementation

{$R *.DFM}


function JHookFunc(Code:integer; wParam: Longint; var EventStrut: TEVENTMSG): Longint; stdcall;
begin
{this is the JournalRecordProc}
Result := CallNextHookEx( hJHook, Code, wParam, Longint(@EventStrut) );
if Code < 0 then Exit;

if Code = HC_ACTION then
  begin
  if (EventStrut.message = WM_KEYDOWN) and
   (EventStrut.paramL = 17923) {17923 is for Ctrl+Break} then
     begin
     UnhookWindowsHookEx(hJHook);  {Ctrl+Break will End the Journal record}
     Journaling := False;
     Form1.ListBox1.Items.Add('JHookFunc has Ended');
     end;
  if (EventStrut.message = WM_KEYUP) then
  if (LoByte(LoWord(EventStrut.paramL)) > 64) and (LoByte(LoWord(EventStrut.paramL)) < 91) then
  Form1.ListBox1.Items.Add('Key UP '+Char(LoByte(LoWord(EventStrut.paramL)))+'  '+IntToStr(EventStrut.paramH))
  else
  Form1.ListBox1.Items.Add('Key UP '+IntToStr(EventStrut.paramL)+'  '+IntToStr(EventStrut.paramH));
  end;
end;

procedure TForm1.button_StartEzJourClick(Sender: TObject);
begin
if Journaling then Exit;
hJHook := SetWindowsHookEx(WH_JOURNALRECORD , @JHookFunc, hInstance, 0);

if hJHook > 0 then
  begin
  Journaling := True;
  ListBox1.Clear;
  Form1.ListBox1.Items.Add('JHookFunc has Started, '+IntToStr(hJHook));
  end else
  ShowMessage('J Hook has FAILED to be set');
end;

procedure TForm1.button_StopEzJourClick(Sender: TObject);
begin
UnhookWindowsHookEx(hJHook);
if Journaling then
ListBox1.Items.Add('JHookFunc has Ended');
Journaling := False;
end;
0
 

Author Comment

by:skynergy
ID: 7137291
I tried the code but get a Variable required error mesage on the following line:
hJHook := SetWindowsHookEx(WH_JOURNALRECORD , @JHookFunc, hInstance, 0);
at the @JHookFunc. Shouldn't there be variables in brackets there?
0
 
LVL 33

Accepted Solution

by:
Slick812 earned 50 total points
ID: 7139173
No, why woud you think that?, look at the Win32 API Help for SetWindowsHookEx( ) you will see that variable as a HOOKPROC, a pointer to a function. Look at the code in the first example and This code works for me in Delphi 5, I don't understand the error message you get, in my windows.pas the SetWindowsHookEx is defined as -

function SetWindowsHookEx(idHook: Integer; lpfn: TFNHookProc; hmod: HINST; dwThreadId: DWORD): HHOOK; stdcall;

and none of the parameters are listed with a var designation. The only difference in this and the function call before is that I included the hInstance to see if that would help. Since that's the only difference, try this -


procedure TForm1.button_StartEzJourClick(Sender: TObject);
var
Inst1: HINST;
begin
if Journaling then Exit;
Inst1 := hInstance;
hJHook := SetWindowsHookEx(WH_JOURNALRECORD , @JHookFunc, Inst1, 0);

if hJHook > 0 then
 begin
 Journaling := True;
 ListBox1.Clear;
 Form1.ListBox1.Items.Add('JHookFunc has Started, '+IntToStr(hJHook));
 end else
 ShowMessage('J Hook has FAILED to be set');
end;

what version of delphi are you using?
0
 

Expert Comment

by:CleanupPing
ID: 9343176
skynergy:
This old question needs to be finalized -- accept an answer, split points, or get a refund.  For information on your options, please click here-> http:/help/closing.jsp#1
EXPERTS:
Post your closing recommendations!  No comment means you don't care.
0

Featured Post

How to run any project with ease

Manage projects of all sizes how you want. Great for personal to-do lists, project milestones, team priorities and launch plans.
- Combine task lists, docs, spreadsheets, and chat in one
- View and edit from mobile/offline
- Cut down on emails

Join & Write a Comment

The uses clause is one of those things that just tends to grow and grow. Most of the time this is in the main form, as it's from this form that all others are called. If you have a big application (including many forms), the uses clause in the in…
Introduction I have seen many questions in this Delphi topic area where queries in threads are needed or suggested. I know bumped into a similar need. This article will address some of the concepts when dealing with a multithreaded delphi database…
Sending a Secure fax is easy with eFax Corporate (http://www.enterprise.efax.com). First, Just open a new email message.  In the To field, type your recipient's fax number @efaxsend.com. You can even send a secure international fax — just include t…
This demo shows you how to set up the containerized NetScaler CPX with NetScaler Management and Analytics System in a non-routable Mesos/Marathon environment for use with Micro-Services applications.

758 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

18 Experts available now in Live!

Get 1:1 Help Now