COM and Services - delphi

I'm trying to create a service to communicate with a COM object that controls on BarCode Scanner (model Motorola Symbol LS9208).
I created the TLB unit from ocx installed with scanner driver application.
In Delphi, I had create a DataModule that make all the work I put the start and stop code of scanner in ServiceExecute procedure:
Initialize the TScanner COM object (declared in TLB)
Set some scanner properties, "claim" the control and set one TScanner event with one of the procedures in DataModule Service to fire when barcode is readed
After all, before close, release and free the TScanner object
procedure TInteliPEDCheckService.ServiceExecute(Sender: TService);
begin
debugLog( 'initialization of service thread...' );
CheckInitParams;
//DataModule_Create;
startScanner;
//loopback
while (not Terminated) do
begin
Sleep(100);
ServiceThread.ProcessRequests(False);
end;
debugLog( 'end of service loop...' ); //only reached if stop service before read codbar
stopScanner;
//DataModule_Free;
debugLog( 'finalization of service thread...' );
end;
procedure TInteliPEDCheckService.startScanner;
var HR : HRESULT;
begin
//CoInitialize( nil );
//CoInitializeEx( nil, COINIT_MULTITHREADED );
//CoInitializeEx( nil, COINIT_MULTITHREADED or COINIT_DISABLE_OLE1DDE );
//CoInitializeEx( nil, COINIT_APARTMENTTHREADED );
CoInitializeEx( nil, COINIT_APARTMENTTHREADED or COINIT_DISABLE_OLE1DDE );
// I've tried all options above, but without success
HR := CoInitializeSecurity(
nil, -1, nil, nil,
1, // RPC_C_AUTHN_LEVEL_NONE
1, // RPC_C_IMP_LEVEL_ANONYMOUS
nil,
0, // EOAC_NONE
nil);
debugLog( 'HRESULT of CoInitializeSecurity: ' + IntToStr( HR ) );
// the log always shows 0 as HRESULT
Scanner := TScanner.Create( nil );
Scanner.Open( 'STI_USBSCANNER' );
if Scanner.Claim(0) = 0 then
begin
Scanner.OnDataEvent := ScannerDataEvent;
//Scanner.OnDirectIOEvent := ScannerDirectIOEvent;
Scanner.DeviceEnabled := TRUE;
Scanner.DataEventEnabled := TRUE;
Scanner.FreezeEvents := FALSE;
end
else
begin
Scanner.DeviceEnabled := FALSE;
Scanner.DataEventEnabled := FALSE;
Scanner.FreezeEvents := FALSE;
end;
// Caption := IfThen( Scanner.Claimed, 'OK', 'FAIL' );
end;
procedure TInteliPEDCheckService.stopScanner;
begin
if ( Scanner <> nil ) then
begin
Scanner.Release;
Scanner.Free;
CoUninitialize();
end;
end;
procedure TInteliPEDCheckService.ScannerDataEvent(ASender: TObject; lStatus: Integer);
begin
debugLog( 'will fire...' ); //never reached
debugLog( 'FIRED: ' + Scanner.ScanData );
//Scanner.DeviceEnabled := FALSE;
//Sleep(1000);
Scanner.DeviceEnabled := TRUE;
Scanner.DataEventEnabled := TRUE;
end;
When I create the DataModule run this code into an EXE application, everything works fine. But now I'm migrating this funcionality to a Windows Service.
As first problem, the error of "CoInitialize not called" fired and nothing works.
So, I tried to CoInitialize() in service's DPR, before Application.Initialize but don't solve, and error keep raising.
The error only gone when I put CoInitialize() just before TScanner.Create(nil), so the scanner starts and I could see the laser ready to read barcodes.
Now the scanner starts fine with the service start, and stop fine too when service stops.
But if I read some barcode, and the event OnDataEvent is about to be triggered, the service is halted, and no data is received, an (of course) I lost the control of the scanner, needing to restart the service.
I read some instructions of how to CoInitialize the COM objects (https://msdn.microsoft.com/en-us/library/windows/desktop/ff485844(v=vs.85).aspx) and found some mention about marshalling technique to transport data between COM objects and other threads. But I'm not sure if this is the problem... and I don't know how to do that.
The service always halt when I scan some barcode. If not, starts and stops without errors, and the laser lights on when start, and light off when stop, demonstrating normal and correct command and operation.
Please, some one can help me to solve this problem?
Thanks in advance!

I once solved COM trouble within an NT-service by calling CoInitializeSecurity right after CoInitialize. I wrote more about it here. Apart from that it is important to get the threading right. So be sure by debugging the 'normal' application version and using GetCurrentThreadId that the incoming data from the scanner is handled by the exact thread you expect it on.

Related

Execute and Wait not working sometimes

I am using this code I found on the Internet and on some devices it waits, but on others it does not. Can someone please explain where I am going wrong. My app loads in Truecrypt and then waits for the user to enter the password. On exiting Truecrypt, it then launches my menu-program.
My Lenovo Miix 2 8" tablet, win8.1 (all up to date) will wait, my Dad's win8.0 (all up to date) will wait, but my friend's ASUS M80TA 8" win8.1 tablet (all up to date) will not. Another friend's win7 laptop (all up to date) does not wait ether.
var
aTSI : TStartupInfo;
aTPI : TProcessInformation;
iRet : Integer;
ExitCode: Cardinal;
begin
FillChar(aTSI, SizeOf(aTSI), #0);
FillChar(aTPI, SizeOf(aTPI), #0);
aTSI.CB:=SizeOf(aTSI);
if not CreateProcess(nil, PChar(sEXE), nil, nil, False,
NORMAL_PRIORITY_CLASS,
nil, nil, aTSI, aTPI) then
RaiseLastWin32Error;
repeat
iRet:=MsgWaitForMultipleObjects(1, aTPI.hProcess,
False, INFINITE, (QS_ALLINPUT));
if iRet <> (WAIT_OBJECT_0) then
Application.ProcessMessages;
until iRet = (WAIT_OBJECT_0); // use this for normal programs
ExitCode:= 0;
if not GetExitCodeProcess(aTPI.hProcess, ExitCode) then
RaiseLastWin32Error;
Result:= ExitCode;
CloseHandle(aTPI.hProcess);
end;
The likely explanation is as follows:
You call CreateProcess which creates a new process and returns a handle to that process.
That first new process in turn starts a different process, and immediately returns. That second process is the one that you see, and believe to be the process that you created.
Your wait on the first process handle returns.
In order to know how to deal with this you'd need to supply some details about the process that you are attempting to start. As to why the code works on some machines and not others, that would likely be down to the implementation details of the target application, the external application that you are starting. Presumably it differs from machine to machine.
Looking at the code, it always leaks the thread handle returned in aTPI.hThread. And it leaks aTPI.hProcess if GetExitCodeProcess fails.
You also need to ensure that the string you pass to the command line argument of CreateProcess is an editable string, and not a literal that is stored in read-only memory.
It is also pointless to initialise ExitCode and then immediately overwrite it. What's more you can remove the ExitCode variable and pass Result directly to GetExitCodeProcess.
Your code also fails to acknowledge an error being returned by the wait function.
I'd probably write it like this:
function ExecAndWait(CommandLine: string): DWORD;
var
si: TStartupInfo;
pi: TProcessInformation;
iRet: Integer;
begin
UniqueString(CommandLine);
si := Default(TStartupInfo);
si.cb := SizeOf(si);
Win32Check(CreateProcess(nil, PChar(CommandLine), nil, nil, False,
NORMAL_PRIORITY_CLASS, nil, nil, si, pi));
CloseHandle(pi.hThread);
try
while True do
begin
iRet := MsgWaitForMultipleObjects(1, pi.hProcess, False, INFINITE, QS_ALLINPUT);
Win32Check(iRet <> WAIT_FAILED);
case iRet of
WAIT_OBJECT_0:
break;
WAIT_OBJECT_0+1:
Application.ProcessMessages;
end;
end;
Win32Check(GetExitCodeProcess(pi.hProcess, Result));
finally
CloseHandle(pi.hProcess);
end;
end;
On my machine, when I pass 'notepad.exe' to this function, the function does not return until the Notepad process is closed.
On the other hand, if I pass 'explorer.exe' to the process, then the function returns immediately. What happens here is that a new explorer process starts, but it detects that one is already running, and asks that process to open a new window. The newly started explorer process immediately terminates.

How do I wait to delete a file until after the program I started has finished using it?

I have been looking for a way to open a file saved to my computer via a Delphi app with its appropriate application. The file is stored in a Varbinary field in a SQL database, and is loaded into a memory stream and then saved via the TMemoryStream's SavetoFile method. What I would like to accomplish is to open the saved file in its appropriate application without knowing the filepath to that application's executable. I have had some success using ShellExecuteEx, but certain applications don't return an HProcess (Windows Live Photo Gallery, for example), so I can't (or at least don't know how to) wait for the application to close before moving on when a handle isn't returned. Is there a way to ensure I receive a handle when calling ShellExecuteEx? If this is not the best way how should I go about doing this?
I only need to know the external app's status because I plan on deleting the file after it closes, and I only need to write it because I'm fairly certain I can't load the file stored in the SQL table into memory (by way of a MemoryStream, FileStream, etc.) and launch its associated program directly from my Delphi app. (I've asked about that separately.)
Trying to detect that the displaying process has closed is brittle and fraught with problems, as you learnt in your previous question. Often times, it's hard to find the process that is used to view the file, and even if you can, there's no certainty the closing the view of the file will close the process. The process may be used to view other files which the user leaves open. I think the lesson that you should take from that is that the system does not want you to do what you are trying to do.
So, what's the better way to solve the problem? I think the best you can do is to create the temporary files in the temporary directory and not attempt to delete them when the user has finished with them. You could:
Remember the files you created and when you create, say the 21st file, delete the first one you made. Then delete the 2nd when you create the 22nd and so on.
Or, delete all temporary files on startup. This would remove files from a previous session.
Or run a separate tidy up thread that, every ten minutes, say, deleted files that were created more than an hour ago.
You get the idea. The point is that it is an intractable problem to detect when the viewer has finished with the file, in full generality. So you need to think creatively. Find a different way around the road block.
Hers a snip from a unit I use for a similar purpose. I found these functions online somewhere over the the years so I take no credit and make no promises.
I personally use the WaitExec() function to launch a pdf (retrieved from a database) in Acrobat for editing and then re-save it to our database when done.
I have used the two other functions at other times as well so I know they all work to one degree or another but I think WaitExec() worked best in an interactive mode, while Launch() worked better from a thread or non-interactive mode.
The IsFileInUse function can tell you if the file you created is in use by any other processes and may be a viable option as well.
uses SysUtils, Windows, ShellAPI, Forms, Registry, Classes, Messages, Printers,
PSAPI, TlHelp32, SHFolder;
function IsFileInUse(fName: string): boolean;
var
HFileRes: HFILE;
begin
Result := False;
if not FileExists(fName) then
Exit;
HFileRes := CreateFile(pchar(fName), GENERIC_READ or GENERIC_WRITE,
0 {this is the trick!}, nil, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, 0);
Result := (HFileRes = INVALID_HANDLE_VALUE);
if not Result then
CloseHandle(HFileRes);
end;
function Launch(sCommandLine: string; bWait: Boolean; AppHandle: HWND): Boolean;
var
SEI: TShellExecuteInfo;
Mask: Longint;
begin
Mask := SEE_MASK_NOCLOSEPROCESS;
FillChar(SEI, Sizeof(SEI), #0);
SEI.cbsize := Sizeof(SEI);
SEI.wnd := AppHandle;
SEI.fmask := Mask;
//if FExeStyleString<>'' then SEI.LPVERB:=pchar(FExeStyleString);
SEI.LPFile := pchar(sCommandline);
//SEI.LPParameters := pchar(FExeParameters);
//SEI.LPDirectory := pchar(FExepath);
SEI.nshow := SW_SHOWNORMAL; // SW_SHOWMINIMIZED, SW_SHOWMAXIMIZED
ShellexecuteEx(#SEI);
if bWait then
WaitforSingleObject(SEI.hProcess, INFINITE);
Result := True;
end;
function WaitExec(const CmdLine:AnsiString;const DisplayMode:Integer):Integer;
{Execute an app, wait for it to terminate then return exit code. Returns -1
if execution fails. DisplayMode is usually either sw_ShowNormal or sw_Hide.}
var
S:TStartupInfo;
P:TProcessInformation;
M:TMsg;
R:DWord;
begin
FillChar(P,SizeOf(P),#0);
FillChar(S,Sizeof(S),#0);
S.cb := Sizeof(S);
S.dwFlags := STARTF_USESHOWWINDOW;
S.wShowWindow := DisplayMode;
if not CreateProcess(nil,
PChar(CmdLine), { pointer to command line string }
nil, { pointer to process security attributes }
nil, { pointer to thread security attributes }
False, { handle inheritance flag }
CREATE_NEW_CONSOLE or { creation flags }
NORMAL_PRIORITY_CLASS,
nil, { pointer to new environment block }
nil, { pointer to current directory name }
S, { pointer to STARTUPINFO }
P) { pointer to PROCESS_INF }
then begin
ShowMessage('Create Process failed. Save this message for IT: ' + CmdLine);
Result:=-1
end
else begin
// WaitforSingleObject(P.hProcess,INFINITE);
// The following replacement better satisfies DDE requirements
repeat
R := MsgWaitForMultipleObjects(1, // One event to wait for
P.hProcess, // The array of events
FALSE, // Wait for 1 event
INFINITE, // Timeout value
QS_ALLINPUT); // Any message wakes up
if R>WAIT_OBJECT_0 then begin
M.Message := 0;
while PeekMessage(M,0,0,0,PM_REMOVE) do begin
TranslateMessage(M);
DispatchMessage(M);
end
end;
until R=WAIT_OBJECT_0;
// put value into Result.... non zero = success
GetExitCodeProcess(P.hProcess,DWord(Result));
CloseHandle(P.hProcess);
CloseHandle(P.hThread);
P.hProcess:=0;
P.hThread:=0;
end;
end;

Delphi: How to send MIDI to a hosted VST plugin?

I want to use VST plugins in my Delphi program which acts as a VST host. I have tried the tobybear examples, used the delphiasiovst stuf, got some of it even working, but... I don't know how to send MIDI messages to the plugin (I am aware that most plugins will not handle MIDI, but I have an example plugin that does).
To be more specific: I expect that when I send a MIDI message, I have to either use one or other method in the VST plugin or reroute the MIDI output. I just don't know how.
Can anyone point me to documentation or code on how to do this? Thanks in advance.
Arnold
I use two test plugins: the one compiled from the DelphiAsioVst package and PolyIblit. Both work in Finale and LMMS. Loaded into my test program both show their VST editor.
I did insert the TvstEvent record and initialized it, I inserted the MIDIData and the AddMIDIData procedures and a timer to provide test data and to execute the ProcessEvents routine of the plugin. ProcessEvents gets the correct test data, but no sound is heard. I hear something when I send it directly to the midi output port.
In the code below the PropcessEvents should be sufficient imho, the additional code is a test whether the MIDI information is correctly sent. VstHost [0] is the first plugin, being either the PolyIblit or the VSTPlugin, depending on the test.
procedure TMain_VST_Demo.TimerTimer (Sender: TObject);
var i: Int32;
begin
// MIDIOutput.PutShort ($90, 60, 127);
MIDIData (0, $90, 60, 127);
if FMDataCnt > 0 then
begin
FMyEvents.numEvents := FMDataCnt;
VSTHost[0].ProcessEvents(#FMyEvents);
// if (FCurrentMIDIOut > 0) and MIMidiThru.Checked then
// begin
for i := 0 to FMDataCnt - 1 do
MIDIOutput.PutShort (PVstMidiEvent (FMyEvents.events[i])^.midiData[0],
PVstMidiEvent (FMyEvents.events[i])^.midiData[1],
PVstMidiEvent (FMyEvents.events[i])^.midiData[2]);
// FMidiOutput.Send(//FCurrentMIDIOut - 1,
// PVstMidiEvent(FMyEvents.events[i])^.midiData[0],
// PVstMidiEvent(FMyEvents.events[i])^.midiData[1],
// PVstMidiEvent(FMyEvents.events[i])^.midiData[2]);
// end;
FMDataCnt := 0;
end;
end; // TimerTimer //
So I don't get the events in the plugin. Any idea what do I wrong?
You should really look at the minihost core example (Delphi ASIO project, v1.4).
There is a use of midi events. Basically
you have a TVstEvents variable ( let's say MyMidiEvents: TvstEvents).
for the whole runtime you allocate the memory for this variable ( in the app constructor for exmaple)
When you have an event in your MIDI callback, you copy it on the TVstEvents stack.
Before calling process in the TVstHost, you call MyVstHost.ProcessEvents( #MyMidiEvents ).
this is how it's done in the example (minihost core), for each previously steps:
1/ at line 215, declaration
FMyEvents: TVstEvents;
2/ at line 376, allocation:
for i := 0 to 2047 do
begin
GetMem(FMyEvents.Events[i], SizeOf(TVSTMidiEvent));
FillChar(FMyEvents.Events[i]^, SizeOf(TVSTMidiEvent), 0);
with PVstMidiEvent(FMyEvents.Events[i])^ do
begin
EventType := etMidi;
ByteSize := 24;
end;
end;
3/ at line 986 then at line 1782, the midi event is copied from the callback:
the callback
procedure TFmMiniHost.MidiData(const aDeviceIndex: Integer; const aStatus, aData1, aData2: Byte);
begin
if aStatus = $FE then exit; // ignore active sensing
if (not Player.CbOnlyChannel1.Checked) or ((aStatus and $0F) = 0) then
begin
if (aStatus and $F0) = $90
then NoteOn(aStatus, aData1, aData2) //ok
else
if (aStatus and $F0) = $80
then NoteOff(aStatus, aData1)
else AddMidiData(aStatus, aData1, aData2);
end;
end;
event copy
procedure TFmMiniHost.AddMIDIData(d1, d2, d3: byte; pos: Integer = 0);
begin
FDataSection.Acquire;
try
if FMDataCnt > 2046
then exit;
inc(FMDataCnt);
with PVstMidiEvent(FMyEvents.events[FMDataCnt - 1])^ do
begin
EventType := etMidi;
deltaFrames := pos;
midiData[0] := d1;
midiData[1] := d2;
midiData[2] := d3;
end;
finally
FDataSection.Release;
end;
end;
4/ at line 2322, in TAsioHost.Bufferswitch, the TVstHost.ProcessEvents is called
FDataSection.Acquire;
try
if FMDataCnt > 0 then
begin
FMyEvents.numEvents := FMDataCnt;
VSTHost[0].ProcessEvents(FMyEvents);
if (FCurrentMIDIOut > 0) and MIMidiThru.Checked then
begin
for i := 0 to FMDataCnt - 1 do
FMidiOutput.Send(FCurrentMIDIOut - 1,
PVstMidiEvent(FMyEvents.events[i])^.midiData[0],
PVstMidiEvent(FMyEvents.events[i])^.midiData[1],
PVstMidiEvent(FMyEvents.events[i])^.midiData[2]);
end;
FMDataCnt := 0;
end;
finally
FDataSection.Release;
end;
this should help you a lot if you were not able to analyse the method used.
If you are hosting VST 2.x plugins, you can send MIDI events to the plugin using AudioEffectX.ProcessEvents().
From the VST docs.
Events are always related to the current audio block.
For each process cycle, processEvents() is called once before a processReplacing() call (if new events are available).
I don't know of any code examples. There might be something in DelphiAsioVST.
If you're up for a change of programming language you could try VST.NET that allows you to write plugins and hosts in C# and VB.NET.
Hope it helps.

Is the code below worth unit testing?

I'm new to unit testing. And I don't know whether it is worth to unit test the code below. Here's sample method written in Delphi:
function TCoreAudio.CreateAudioClient: IAudioClient;
var
MMDeviceEnumerator: IMMDeviceEnumerator;
MMDevice: IMMDevice;
MixFormat: PWaveFormatEx;
AudioClient: IAudioClient;
HR: HResult;
begin
Result := nil;
if CheckWin32Version(6, 0) then // The Core Audio APIs were introduced in Windows Vista.
begin
HR := GetInstance().CoCreateInstance(CLSID_MMDeviceEnumerator, nil, CLSCTX_ALL,
IMMDeviceEnumerator, MMDeviceEnumerator);
if Failed(HR) then
Exit;
HR := MMDeviceEnumerator.GetDefaultAudioEndpoint(eRender, eConsole, MMDevice);
if Failed(HR) then
Exit;
HR := MMDevice.Activate(IAudioClient, CLSCTX_ALL, nil, AudioClient);
if Failed(HR) then
Exit;
HR := AudioClient.GetMixFormat(MixFormat);
if Failed(HR) then
Exit;
HR := AudioClient.Initialize(AUDCLNT_SHAREMODE_SHARED, 0, 0, 0, MixFormat, nil);
CoTaskMemFree(MixFormat);
if Failed(HR) then
Exit;
Result := AudioClient;
end;
end;
Is that method worth unit testing? If it is, what parts of it need to be tested?
Thank You.
The problem you face is how to test it rather then whether it should be tested.
This is a wrapper to a number of COM calls which could fail for many different reasons. Those possible COM failure conditions are the most important aspects to test for this routine. But you can't easily provoke the COM routines to fail. In order to test these COM failure modes you'd need to use a mock and that's quite a leap from where you are.
Unit testing is usually a bottom-up approach. So you would start unit testing the classes that are used in your function. After having made sure that all these classes are covered by unit tests, you could create a unit test for your function CreateAudioClient. The unit test for this function is probably very easy, something like this:
AudioClient := CreateAudioClient;
CheckNotNil (AudioClient);
Note that you normally unit test the interface of a class and not the body of a function or procedure.
Hope that helps.
The question if it is worth it, depends on a number of factor:
How easy is it to build a unit test for it? How much effort would it be?
How important is this part? Depending on how critical this part is, the answer may be always "yes, it is definitely worth unit testing" - or not.
How likely is it to change? If you think this method might change somewhere in the future then adding a unit tests avoids introducing errors later.
When unit testing a class that acts as an interface between your application and a third party API (even a system API) you want to test that the class calls and responds to the API correctly. You can't do this without some way to sense what is being passed to the API and return an appropriate response.
In your case you are making a series of calls to obtain an IAudioClient. I'd say your doing too much. More than one conditional in a function is one conditional too many (I think I just confused myself with that one). I would break it into several functions that you can test individually.
function TCoreAudio.CreateAudioClient: IAudioClient;
var
MMDeviceEnumerator: IMMDeviceEnumerator;
MMDevice: IMMDevice;
MixFormat: PWaveFormatEx;
AudioClient: IAudioClient;
begin
Result := nil;
if IsVista then
try
MMDeviceEnumerator := GetMMDeviceEumerator;
MMDevice := GetMMDevice(MMDeviceEnumerator);
AudioClient := GetAudioClient(MMDevice);
MixFormat := GetMixFormat(AudioClient);
InitializeAudioClient(AudioClient, MixFormat);
Result := AudioClient;
except
//Handle exception
end;
end;
function TCoreAudio.IsVista: boolean;
begin
Result := CheckWin32Version(6, 0);
end;
function TCoreAudio.GetMMDeviceEnumerator: IMMDeviceEnumerator;
begin
HR := GetInstance().CoCreateInstance(CLSID_MMDeviceEnumerator, nil, CLSCTX_ALL,
IMMDeviceEnumerator, Result);
if Failed(HR) then
raise Exception.Create('Failed to create device enumerator');
end;
function TCoreAudio.GetMMDevice(ADeviceEnumerator: IMMDeviceEnumerator): IMMDevice;
begin
HR := MMDeviceEnumerator.GetDefaultAudioEndpoint(eRender, eConsole, Result);
if Failed(HR) then
raise Exception.Create('Failed to retrieve device');
end;
function TCoreAudio.GetAudioClient(ADevice: IMMDevice): IAudioClient;
begin
HR := MMDevice.Activate(IAudioClient, CLSCTX_ALL, nil, Result);
if Failed(HR) then
raise Exception.Create('Failed to retrieve audio client');
end;
function TCoreAudio.GetMixFormat(AAudioClient: IAudioClient): PWaveFormatEx
begin
HR := AudioClient.GetMixFormat(Result);
if Failed(HR) then
raise Exception.Create('Failed to retrieve mix format');
end;
procedure TCoreAudio.InitializeAudioClient(AAudioClient: IAudioClient, AMixFormat: PWaveFormatEx);
begin
HR := AudioClient.Initialize(AUDCLNT_SHAREMODE_SHARED, 0, 0, 0, AMixFormat, nil);
CoTaskMemFree(MixFormat);
if Failed(HR) then
raise Exception.Create('Audio client failed to initialize');
end;
Now you can provide a mock/fake/stub to each function, ensuring the API is being called with appropriate arguments and forcing failure conditions to make sure your production code is handling them properly.
You don't need to ask if production code should be tested. The answer is always yes. (warning:shameless self-promotion) I wrote about this recently on my blog. Sometimes even the most innocuous of all statements, the assignment statement, doesn't work as expected.
Actually now that its broken down its starting to look like a new creational class just itching to break out.
it really depends on how often you believe changes will be made to this method... if you already have some tests for it's unit, then yes do it, otherwise it's really up to you, but I'm not sure it's a good idea to test only this method from the whole unit, because it calls other methods from other units.
Bottom line, it's really up to you how you choose to do it, best way is to add testing to whole project, otherwise I can't really see any value in "partial testing".

how to open additional files into an already running application

I am writing a MDI Text Editor and I was wondering how can I open all text files with my app. (If I associate te *.txt to my app I want that each time someone double-clicks on a txt file to open it in my app, in a new child window)
thanks
The solution to this is also the solution to not allowing more than one application to run at the same time. What you want to do is first detect that the program is already running, then pass a parameter to the running application and shut down.
There are several methods to determine if your application is already running. Once you pick one that fits your programming preferences, the next step is to feed the file to open to your running program. This can be done via named pipes, messages (although messages do fail on Vista/Win7 if your app is running in another security context), or any other method of IPC.
I Currently have the following implementation for this :
The .dpr file
var
PrevWindow : HWND;
S : string;
CData : TCopyDataStruct;
begin
PrevWindow := 0;
if OpenMutex(MUTEX_ALL_ACCESS, False, 'YourUniqueStringHere') <> 0 then
begin
PrevWindow:=FindWindow('TYourMainFormClassName', nil);
if IsWindow(PrevWindow) then
begin
SendMessage(PrevWindow, WM_SYSCOMMAND, SC_RESTORE, 0);
BringWindowToTop(PrevWindow);
SetForegroundWindow(PrevWindow);
if FileExists(ParamStr(1)) then
begin
S:=ParamStr(1);
CData.dwData:=0;
CData.lpData:=PChar(S);
CData.cbData:=1+Length(S);
SendMessage(PrevWindow, WM_COPYDATA, 0, DWORD(#CData) );
end;
end;
end
else
CreateMutex(nil, False, 'YourUniqueStringHere');
in the main unit we process the WM_COPYDATA message :
we declare the message handler
procedure ReceiveData_Handler ( var msg : TWMCopyData ) ; message WM_COPYDATA;
procedure TForm1.ReceiveData_Handler(var msg: TWMCopyData);
begin
// Your file name is in the msg.CopyDataStruct.lpData
// Cast it to PChar();
end;
Hope it works for you.
Check out the Windows DDE documentation. I modify the DDEExec options in the registry, so the shell correctly directs the opened file to my existing application instance. The following code makes the registry changes necessary. Replace "AppName" with your application name (and remove the brackets).
// add the ddeexec key
if not reg.OpenKey( '\Software\Classes\<AppName>.file\shell\open\ddeexec', true ) then
raise Exception.Create( 'Error setting ddeexec key' );
try
reg.WriteString( '', 'FileOpen("""%1""")' );
finally
reg.CloseKey;
end;
// modify the command key to not include the parameter, as we don't use it
if not reg.OpenKey( '\Software\Classes\<AppName>.file\shell\Open\command', true ) then
raise Exception.Create( 'Error opening command key.' );
try
strTemp := reg.ReadString( '' );
strTemp := StringReplace( strTemp, '"%1"', '', [] );
reg.WriteString( '', strTemp );
finally
reg.CloseKey;
end;
I don't know the version of Delphi that you're using, but in Delphi 7 at the examples folder you will see a MDI Text Editor example.

Resources