Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

DDE Server (for single-instance file association) [std-lib]


  • Please log in to reply
67 replies to this topic
Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006
Dynamic Data Exchange Server
The purpose of this script is to allow Explorer (or other DDE clients) to send commands to a running instance of a script. Why?

The Example

File type associations. Say you've written a tabbed document viewer in AutoHotkey. When you launch a document, you'll want it to open in a new tab of an existing viewer. There is at least one obvious solution:Start up another instance of the script,
have it send a message to the existing viewer to open the document,
then exit.The problem with this approach is that it must start one (temporary) instance of the script for each document that you launch. This might not be ideal if you need to open a bunch of documents at once.

DDE, the optimal solution: using DDE, you can have Explorer send a message to your script. Now you can start the script once for all documents.

DDE_InitServer() is all you - as an application programmer - need to be concerned with. DDE_Initiate(), DDE_Execute() and DDE_Terminate() handle the necessary DDE messages for you. DDE_Execute() will call a label of your choosing.

A working example script (requires DDE.ahk):
#NoEnv

; Initialize the DDE server.
DDE_InitServer("AutoHotkey DDE Test", "Launch", "DDE_Command")

; Let the user know if command-line args were specified.
n = 1
if 0 > 0 ; var '0' contains the number of args
    MsgBox % "args: " %n% ; access arg '1' indirectly via 'n'

return

; When you double-click a file (or select the appropriate action from the
; context menu), assuming the file type is associated correctly the script will
; call this sub-routine.
DDE_Command:
    ; For now just show what command was received.
    MsgBox %DDE_Cmd%
return
(See further down for the .reg file accompanying this example.)

The Function

This may be saved in your user function library (%A_MyDocuments%\AutoHotkey\Lib) as DDE.ahk - this allows the file to be auto-included from any script that calls DDE_InitServer(). Otherwise, you'll need to manually #include the file.
; DDE_InitServer( ApplicationName, TopicName, LabelName [, LeaveCriticalOn ] )
;   Sets up the script to receive DDE "execute" requests. This function should
;   be called at the top of the script in order to support associating file
;   types with scripts via DDE. (See the comment above "Critical 500".)
;
; ApplicationName:
;   Identifies the application/DDE server.
;   Windows XP: This is the "Application" field of the file type action dialog.
;
; TopicName:
;   The topic of DDE "conversation".
;   Windows XP: This is the "Topic" field of the file type action dialog.
;               Since this field cannot be empty, TopicName must be specified.
;
; LabelName:
;   The label to execute when WM_DDE_EXECUTE is received.
;   DDE_Cmd will contain the command-line to execute.
;   In Windows XP, this is one of the following fields, depending on whether
;     the script was running when the relevant action was executed:
;         DDE Message
;         DDE Application Not Running
;     (If the latter is not specified, the former is used in its stead.)
;
; LeaveCriticalOn:
;   Since "Critical" must be used for reliability, this function also disables
;   Critical before returning. If the script must perform other actions that
;   require Critical to be enabled, specify true for LeaveCriticalOn.
;
; Windows XP notes may also apply to earlier versions of Windows.
;
DDE_InitServer(ApplicationName, TopicName, LabelName, LeaveCriticalOn=false)
{
    global
    ;
    ; The DDE server MUST be ready to process DDE requests before the AutoHotkey
    ; window becomes "idle", otherwise launching a document will result in
    ; "Windows cannot find '<document name>'. ..."
    ;
    ; '500' ensures no messages are processed (thus the window appears non-idle)
    Critical 500      ; for at least the next 500 milliseconds.
    
    DDE_Application := ApplicationName
    DDE_Topic := TopicName
    DDE_Label := LabelName
    
    DetectHiddenWindows, On
    Process, Exist
    DDE_hwnd := WinExist("ahk_class AutoHotkey ahk_pid " ErrorLevel)
    DetectHiddenWindows, Off
    
    OnMessage(0x3E0, "DDE_OnInitiate")
    OnMessage(0x3E8, "DDE_OnExecute")
    OnMessage(0x3E1, "DDE_OnTerminate")

    if ! LeaveCriticalOn
        Critical Off
}

DDE_OnInitiate(wParam, lParam)    ; handles WM_DDE_INITIATE
{
    global DDE_Application, DDE_Topic, DDE_hwnd
    
    if DDE_GlobalGetAtomName(lParam&0xFFFF) != DDE_Application
        return
    if DDE_GlobalGetAtomName(lParam>>16) != DDE_Topic
        return
    
    if (atomApplication := DllCall("GlobalAddAtom","str",DDE_Application)) != 0
    {
        if (atomTopic := DllCall("GlobalAddAtom","str",DDE_Topic)) != 0
        {
            DetectHiddenWindows, On
            ; Send WM_DDE_ACK with correct application and topic names.
            SendMessage, 0x3E4, DDE_hwnd, atomApplication | atomTopic<<16,, ahk_id %wParam%
            DllCall("GlobalDeleteAtom","ushort",atomTopic)
        }
        DllCall("GlobalDeleteAtom","ushort",atomApplication)
    }
}

DDE_OnExecute(wParam, lParam)     ; handles WM_DDE_EXECUTE
{
    global DDE_hwnd, DDE_Label, DDE_Cmd
    
    if DllCall("UnpackDDElParam","uint",0x3E8,"uint",lParam,"uint*",0,"uint*",hCmd)
    {
        DDE_Cmd := DllCall("GlobalLock","uint",hCmd,"str"), DllCall("GlobalUnlock","uint",hCmd)
        gosub %DDE_Label%
        success := true
    }
    lParam := DllCall("ReuseDDElParam","uint",lParam,"uint",0x3E8
        ,"uint",0x3E4,"uint",success ? 1<<15 : 0,"uint",hCmd)
    DetectHiddenWindows, On
    ; Acknowledge the request. (WM_DDE_ACK)
    PostMessage, 0x3E4, DDE_hwnd, lParam,, ahk_id %wParam%
}

DDE_OnTerminate(wParam)           ; handles WM_DDE_TERMINATE
{
    global DDE_hwnd
    DetectHiddenWindows, On
    ; Acknowledge WM_DDE_TERMINATE by posting another WM_DDE_TERMINATE.
    PostMessage, 0x3E1, DDE_hwnd, 0,, ahk_id %wParam%
}

DDE_GlobalGetAtomName(atom) {   ; helper function
    if !atom
        return ""
    VarSetCapacity(name,255) ; maximum size of an atom string
    return DllCall("GlobalGetAtomName","ushort",atom,"str",name,"int",256) ? name : ""
}
Changes:[*:259ehb55]DDE_GlobalGetAtomName - Replaced arbitrary buffer size (1024) with maximum size of an atom string (255.)
Registering File Types

In order to have Explorer launch your script automatically - and send DDE messages to it - when you double-click the file or select an item from the context menu, you'll need to do a bit of work.

Windows XP

In Windows XP and earlier, this can be done without diving into the registry. Since I run only Windows XP and Vista, my instructions will be based on Windows XP. Some (perhaps most) of the steps may be relevant to older versions of Widnows. The steps are as follows:
[*:259ehb55]In an Explorer window, select Folder Options from the Tools menu.
[*:259ehb55]Select the File Types tab.
[*:259ehb55]Create a new file type or select an existing file type.
[*:259ehb55]To create a new file type, click New, type the File Extension you wish to associate with your script (for instance ahk or txt), then click OK.
[*:259ehb55]To select an existing file type, simply find its entry in the list and click it.[*:259ehb55]Click Advanced.
[*:259ehb55]If this is a new file type, you may want to enter a description (like "AutoHotkey Script") in the box at the top, or change the file type's icon (by clicking Change Icon.)
[*:259ehb55]Now decide what your action will be. For instance, it could be "open", "edit", "view", or whatever you want.
[*:259ehb55]If your action already exists, select it and click Edit.
[*:259ehb55]Otherwise, click New.If you are editing an existing action, you may want to skip some of the following steps.
[*:259ehb55]Type the name of the action (open, edit, etc.) into the Action field. Preceding a character with ampersand (&) will turn the character into a shortcut key for the action's entry in the context menu. (For instance, pressing "o" will execute "&Open".)
[*:259ehb55]Type the relevant path in the Application used to perform action field.
[*:259ehb55]For compiled (.exe) scripts, type the path of the script executable.
[*:259ehb55]For uncompiled (.ahk) scripts, type the path of AutoHotkey, followed by a space and the path of the script (enclosed in quote marks.)[*:259ehb55]Append any needed command-line arguments to the end of this field. If it does not include %1, XP will automatically append it. To support paths with spaces, %1 must be enclosed in quote marks. When the action is executed, %1 is automatically replaced with the path of the selected file.
[*:259ehb55]Ensure there is a check-mark next to Use DDE.
[*:259ehb55]The Application and Topic fields should be the same as the values you passed to DDE_InitServer() via the ApplicationName and TopicName parameters.
[*:259ehb55]Type a message/command in the DDE Message and DDE Application Not Running fields. If the script is not already running when you execute the action, Explorer will start the script and send (only) the DDE Application Not Running message. If you leave it empty, the DDE Message is sent in either case.
When either message is received, DDE.ahk calls (via gosub) the label/subroutine you passed to DDE_InitServer(), and the global variable DDE_Cmd is set to the value of the message. Again, %1 is automatically replaced with the path of the selected file.
[*:259ehb55]To finish, click OK. If you want the action to be performed when you double-click a file of the associated type, you may need to select the action and click Set Default. (However, the first action you create should automatically be the default.)[/list]These are the settings I used while testing the example script:
Posted Image
While MSDN recommends the message format: [command][command(parameters)] (one or more commands may be specified; parameter list is optional), any message format is likely to work. It is up to the application/script to parse the message.

Editing The Registry

At some point you may need to bypass the GUI method and go straight to the registry. For instance, you could write an installer for your application that writes the relevant registry keys using RegWrite. If you are using Windows Vista, you will need to edit the registry (using regedit or the RegWrite command), since Vista dumbs-down the file type association GUI.

I won't describe how to use RegWrite or regedit; however, I will describe the basic needed registry structure.

First, a couple of notes:
Each key in the registry can have a default value. In regedit, this is displayed as (Default). When writing or reading this value with RegWrite/RegRead, ValueName should be omitted or blank. In .reg files, default values are denoted as '@':
[path\to\registry\key]
@="the key's default value"
All of the following keys are contained in HKEY_CLASSES_ROOT. Bold text indicates a key name, while bold-italicized text indicates a key whose name varies.

.extension - for instance, .ahk
This key can either contain the keys as described below, or redirect to another key. If the keys are directly descended from this key, you may find oddities like having two "Open" actions.
(Default) - the name of the registry key to redirect to. The File Types GUI in Windows XP uses an automatic name like "ft000001".
keyname - for instance, ft000001 or ahkfile
The root key that contains the file type association settings. Its name depends on the (Default) value as described above.
(Default) - contains a description of the file type. For instance, "AutoHotkey Script".
[*:259ehb55]shell
(Default) - the name of the registry key containing the default (double-click) action. (This could be the name of the key below.)
[*:259ehb55]action_name - for instance open, view or edit.
A registry key representing an action in Explorer's context menu.
(Default) - the text to display for this action in Explorer's context menu. Prefix a character with ampersand (&) to turn the character into a shortcut key.
[*:259ehb55]command
(Default) - the path and command-line arguments of the application to execute. For instance, it could be the path to AutoHotkey followed by the path of an uncompiled script. It does not need to contain %1 (the file path), since this can be sent via DDE.
[*:259ehb55]ddeexec
(Default) - the message/command to send to the script. This should contain %1 which is automatically replaced with the path of the selected file.
[*:259ehb55]Application
(Default) - identifies the application/script. This must be the same as the ApplicationName you pass to DDE_InitServer().
[*:259ehb55]IfExec - optional.
(Default) - the message/command to send to the script if the script isn't already running. If the IfExec key doesn't exist or this value is empty/not set, the message/command specified in ddeexec\(Default) is used instead.
[*:259ehb55]Topic
(Default) - the topic of the DDE conversation. This must be the same as the TopicName you pass to DDE_InitServer().
[/list][/list][/list][/list]Example Registry File

Below is the result of exporting the relevant registry keys I used to test the script. I used the example script which can be found closer to the top of this post (Z:\dde.ahk), and tested with two files: test.testfile and Copy of test.testfile (both empty.)
Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\.testfile]
@="ft000001"

[HKEY_CLASSES_ROOT\ft000001]
"EditFlags"=dword:00000000
"BrowserFlags"=dword:00000008
@="DDE Test File"

[HKEY_CLASSES_ROOT\ft000001\shell]
@="open"

[HKEY_CLASSES_ROOT\ft000001\shell\open]
@="&Open"

[HKEY_CLASSES_ROOT\ft000001\shell\open\command]
@="\"C:\\Program Files\\AutoHotkey\\AutoHotkey.exe\" \"Z:\\dde.ahk\" \"%1\""

[HKEY_CLASSES_ROOT\ft000001\shell\open\ddeexec]
@="[open(\"%1\")]"

[HKEY_CLASSES_ROOT\ft000001\shell\open\ddeexec\Application]
@="AutoHotkey DDE Test"

[HKEY_CLASSES_ROOT\ft000001\shell\open\ddeexec\IfExec]
@="[open]"

[HKEY_CLASSES_ROOT\ft000001\shell\open\ddeexec\Topic]
@="Launch"


Paulo-nli
  • Guests
  • Last active:
  • Joined: --
It is great lexikos :D
Thanks :!:

Terrapin as Guest
  • Guests
  • Last active:
  • Joined: --
Thank you very much, lexicos! I can deal with the registry, at least on xp and 2k. I have vista installed in a dual boot but have hardly touched it. I think people with vista could probably do the work there, by hand, if they need this functionality.

I need to study the DDE part. I have saved your post, and will look it over and see what I can come up with. I'm excited - I had begun to figure it couldn't be done. My little app can load 30 files the way I'm doing it, over and over, then screw up with just two. And the processor goes haywire! :p

This looks like just what I need. I may have questions later, I thank you again.

Bob

Sean
  • Members
  • 2462 posts
  • Last active: Feb 07 2012 04:00 AM
  • Joined: 12 Feb 2007

I need to study the DDE part.

Then, I suggest to try to translate it into DDEML format which is more elegant IMO.

TerrapinII
  • Members
  • 7 posts
  • Last active: Dec 01 2007 12:54 AM
  • Joined: 20 Nov 2007
lexicos! It works! :D

Took me about an hour to carefully get the proper key/values in for one of the filetypes I am looking for. I re-read, apparently I must programmatically (RegWrite) the info into the registry for Vista? As there is no interface in Explorer for the user to enter something like Open With? I read it backwards.

I have, in my app, commented out the old filename-catching code, and substituted appropriate code. I did not study your functions other than understanding the use of the one.

It has been a long time since I worked with a .reg file. So had to do some guessing there, as I was entering manually. The ddeexec default value has 'open(... etc. I was receiving that as well as the filename, I tried substituting simply %1%, and it works like a charm with my existing code.

A bit of the trick is deciding how long to wait, before going ahead and deciding that all the files have been received.

I have something like this:
DDE_Label:
    SetTimer, Injectfiles, 600
    Filenames .= DDE_Cmd "`n"
return

Injectfiles:
   SetTimer, Injectfiles, Off
   .... process filenames as usual
   ... basically jump into a point in guidropfiles

They show up in my listview! SingleInstance is on now.

Some filetypes (audio such as flac, mp3, etc) will be on the context menu for my app, only one is opened as it is a filetype I created. I might have some questions yet. I appreciate your help so much. You explained everything beautifully.

Sean: Thank you, I do not know what DDEML is, if this works as I'm doing it, for folks in general, I'm very happy. I have read many of your articles with interest.

Bob

Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006

I re-read, apparently I must programmatically (RegWrite) the info into the registry for Vista? As there is no interface in Explorer for the user to enter something like Open With? I read it backwards.

Programmatically, or with Regedit. Vista has the "Default Programs" interface, which looks nice but is too simplistic to be of use. You can't even add/edit context menu actions with it, let alone set up DDE.

The ddeexec default value has 'open(... etc. I was receiving that as well as the filename, I tried substituting simply %1%, and it works like a charm with my existing code.

That is what I would use. I left [open("%1")] in the article since the DDE documentation says that's how it should be. There doesn't seem to be any (other) reason to stick with that format, though.

Sean: Thank you, I do not know what DDEML is, if this works as I'm doing it, for folks in general, I'm very happy.

I can answer that (with a quote :roll:.)

The Dynamic Data Exchange Management Library (DDEML) provides an interface that simplifies the task of adding DDE capability to an application.

...but it seems to me no more "simplified" than the methods I used. Since I had no prior knowledge of DDE, I learnt from the DDE documentation (not DDEML), writing the script as I went. (I wanted an understanding of DDE, not of some higher-level function library.)

TerrapinII
  • Members
  • 7 posts
  • Last active: Dec 01 2007 12:54 AM
  • Joined: 20 Nov 2007
I appreciate it, again. It is working great for me, and I have the code in place now which sets up the DDE keys for the file extensions. I will pass along, in case anyone finds it helpful... I still occasionally had problems (this is somewhat a part I did not understand) with the server not being ready for the first file with a launch. I sometimes was getting the message of the file not being found. I solved it by placing a Sleep 50 after calling the initialization. I think maybe the reason this works is that it forces the script to then immediately check the message queue.

Also, quotes around %1 can be omitted, as they come with the filename otherwise. it does not matter if there are spaces in the path\filename, so it saves me having to strip them off.

I am very grateful for the work you did. I would never have gotten it.

Bob

Joy2DWorld
  • Members
  • 562 posts
  • Last active: Jun 30 2014 07:48 PM
  • Joined: 04 Dec 2006
Looks NICE (and clean!).


two tiny suggest (if helpful):


1) rename some of the functions to avoid conflict with my DDE (client) Function... would nicely allow easy complete 'DDE' function set.

2) flesh out by adding Request handler. (Poke likely nice to have but execute should handle all functionality. )



note: With your DDE SERVER function, ahk scripts can now EASLY communicate with eachother... (but request handling would make polling more realistic,)


anyhow,


nice job!
Joyce Jamce

Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006

I sometimes was getting the message of the file not being found. I solved it by placing a Sleep 50 after calling the initialization. I think maybe the reason this works is that it forces the script to then immediately check the message queue.

;
    ; The DDE server MUST be ready to process DDE requests before the AutoHotkey
    ; window becomes "idle", otherwise launching a document will result in
    ; "Windows cannot find '<document name>'. ..."
    ;
    ; '500' ensures no messages are processed (thus the window appears non-idle)
    Critical 500      ; for at least the next 500 milliseconds.

Is DDE_InitServer() the first function you call (from the auto-execute section)? If not, AutoHotkey is probably checking for messages before it starts, which lets Explorer return from WaitForInputIdle(). Explorer would then attempt to connect to the server too early, and would give you a "Windows cannot find..." message.

@Joy2DWorld: I'll rename it when I get time. DDE_InitServer() will probably become DdeServerInit(), which might be better anyway.

With your DDE SERVER function, ahk scripts can now EASLY communicate with eachother...

Honestly, majkinetor's IPC module is just as easy to use, and uses less code.

Joy2DWorld
  • Members
  • 562 posts
  • Last active: Jun 30 2014 07:48 PM
  • Joined: 04 Dec 2006

DDE_InitServer() will probably become DdeServerInit(), which might be better anyway.



DDE_ is cool & even helpful,

DDE_InitServer() is GREAT!!

it's the DDE_Execute

and such... that would update... ie. DDE_Execute_IN or such...


just suggest.
Joyce Jamce

Sean
  • Members
  • 2462 posts
  • Last active: Feb 07 2012 04:00 AM
  • Joined: 12 Feb 2007

note: With your DDE SERVER function, ahk scripts can now EASLY communicate with eachother... (but request handling would make polling more realistic,)

I think it can be elegantly done through DDEML, not through DDE Message.

Sean
  • Members
  • 2462 posts
  • Last active: Feb 07 2012 04:00 AM
  • Joined: 12 Feb 2007
I just wrote a quick example. It needs my own version of DDE.ahk.
You may rename it and use #Include to avoid the conflict with lexicos's DDE.ahk.

Server:
#Persistent
OnExit, DdeClose

idInst	:= DDE_Initialize(0, RegisterCallback("DDE_Callback"))
bSucc	:= DDE_NameService(idInst, "AutoHotkey")
Return

DdeClose:
DDE_Uninitialize(idInst)
ExitApp

DDE_Callback(nType, nFormat, hConv, hString1, hString2, hData, nData1, nData2)
{
	If	nType = 0x4050
	{
		Run, %	DDE_GetData(hData)
		DDE_FreeDataHandle(hData)
		Return	0x8000
	}
	Else If	nType = 0x1062
		Return	True
}
Client:
sType	:= "EXECUTE" ; sTopic will use the same here.
sItem	:= ""
sData	:= "http://www.autohotkey.com/forum/"

idInst	:= DDE_Initialize()
hConv	:= DDE_Connect(idInst, "AutoHotkey", sType)
DDE_ClientTransaction(idInst, hConv, sType, sItem, &sData, StrLen(sData)+1) ; synchronous call
DDE_Disconnect(hConv)
DDE_Uninitialize(idInst)


majkinetor
  • Moderators
  • 4512 posts
  • Last active: May 20 2019 07:41 AM
  • Joined: 24 May 2006
Great work lex.
Posted Image

majkinetor
  • Moderators
  • 4512 posts
  • Last active: May 20 2019 07:41 AM
  • Joined: 24 May 2006
Seans version also suffers from the same sympthom as Lex without critcal. It happens here only if single file is selected, doesn't happen for multiple files. Critical solves the problem, again.

Sens version is more advanced as it can be used for both client and server purposes, but Lex interface is more in AHK spirit.

One other important thing is that Seans version is much much faster. When I select multiple files and do context menu action, I can see ListBox populating pretty slow, like one 1 file is added every 300ms. In Sean's version this period is much shorter - around 50ms here.

------ EDIT -----
Acctually, I can not make it to work without displaying File not found message no matter what I do (talking about Seans version now). I used critical with different values and it randomly reports file not found. I noticed that this happens only when 1 file is selected, it never happens with multiple selection.
Posted Image

Joy2DWorld
  • Members
  • 562 posts
  • Last active: Jun 30 2014 07:48 PM
  • Joined: 04 Dec 2006

One other important thing is that Seans version is much much faster. When I select multiple files and do context menu action, I can see ListBox populating pretty slow, like one 1 file is added every 300ms. In Sean's version this period is much shorter - around 50ms here.



Curious why that is.

@Sean, any idea ?


ps: adding example to handle poke and request receptions would likely be helpful.
Joyce Jamce