Using the Windows 95 File Dialog Box in VisualWorks
By Thomas Sattler

- The Smalltalk Report -
October 1997 issue

This contains the text that was not included in the magazine 
due to length restrictions

Let’s call our new class FileDialogInterface:
Object subclass: #FileDialogInterface
	instanceVariableNames: 'fileFilter filterIndex fileName initialDirectory titleBarText flags defaultExtention returnedFilePath options '
	classVariableNames: ''
	poolDictionaries: ''
	category: 'Windows-External Applications'!

Build it as a normal class, directly subclassed from Object. This is the only class with which your application will interact; the Win32CommonDialogs class we just finished building will essentially be private to this class. Every parameter that might be passed to the DLL will have an instance variable and a ‘Translated’ method, which will return it in the format that DLLCC needs. Create normal get and set methods for each instance variable; make the get method for options look like this:
options
	options isNil ifTrue: [ self initializeOptions ] .
	^options!
Add the following private method:
InitializeOptions
	options := IdentityDictionary new 
	at: #allowMultiSelect put: false ;
	at: #createPrompt put: false ;
	at: #enableHook put: false ;
	at: #enableTemplate put: false ;
	at: #enableTemplateHandle put: false ;
	at: #extensionDifferent put: false ;
	at: #fileMustExist put: false ;
	at: #hideReadonly put: false ;
	at: #noChangeDir put: false ;
	at: #noReadonlyReturn put: false ;
	at: #noTestFileCreate put: false ;
	at: #noValidate put: false ;
	at: #overwritePrompt put: false ;
	at: #readonly put: false ;
	at: #shareAware put: false ;
	at: #shareFallThrough put: false ;
	at: #shareNoWarn put: false ;
	at: #shareWarn put: false ;
	at: #showHelp put: false ;
	yourself!

Now we will be able to add options individually, using these methods:
!FileDialogInterface methodsFor: 'options'!

addOption: aSymbol
	(self options includesKey: aSymbol)
		ifFalse: [ self error: aSymbol, ' is not a valid option.' ] .
	self options at: aSymbol put: true!
removeOption: aSymbol
	(self options includesKey: aSymbol)
		ifFalse: [ self error: aSymbol, ' is not a valid option.' ] .
	self options at: aSymbol put: false! !

It’s important to guard against the same option being added twice. Flags is simply an integer, consisting of all options being added together. If we allowed the user to individually add options, it would be possible to add the same option twice, which would have the effect of turning that option off and another option on. For instance, SHOWHELP is 0x10, or integer 16. If we added it twice, the Flags would have a value of 0x20, or 32, which would turn on the option ENABLEHOOK. The file dialog box would be looking for a user-specified hook function, certainly not our intention. So we just say addOption: aSymbol, and in a dictionary we set that key’s value to true. Nothing can be affected by doing it multiple times.
Now let’s build the translation methods. The only ones that need explanation are fileFilterTranslated, optionsTranslatedFrom:, and windowOwnerTranslated.
The fileFilterTranslated method is a bit odd due to the requirement of Windows. The operating system wants the filters in a specific form, and that form is pairs of null-terminated strings, with the first being the description and the second being the filter. An additional null character signals the end of the filters. But we’re storing the filters in the fileFilter instance variable as a collection of associations, so we iterate through the collection and build the string that Windows needs.
The optionsTranslatedFrom: needs to be passed an instance of the Win32CommonDialogs class as a parameter, since it has to find the option values and the Win32CommonDialogs class is the only place that they are defined.
The windowOwnerTranslated method needs to return the windowHandle of the current window’s owner. We can’t get that in Smalltalk, so we have to build another hook into another DLL. Thankfully, this is pretty simple. The entire class is defined below; we’ll call it WindowsUserDLL:

ExternalInterface subclass: #WindowsUserDLL
	includeFiles: 'windows.h '
	includeDirectories: 'c:\vc\include\ '
	libraryFiles: 'USER32.DLL '
	libraryDirectories: ''
	generateMethods: ''
	beVirtual: false
	optimizationLevel: #full
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: 'WindowsUserDLLDictionary '
	category: 'Windows-External Applications'!

!WindowsUserDLL methodsFor: 'types'!

HANDLE
	<C: typedef PVOID HANDLE>!

HWND
	<C: typedef HANDLE HWND>!

PVOID
	<C: typedef void * PVOID>!

!WindowsUserDLL methodsFor: 'procedures'!

GetActiveWindow
	<C: HWND GetActiveWindow(void)>
	^self externalAccessFailed! !

This is about the simplest ExternalInterface subclass you’re ever likely to find. You only need say WindowsUserDLL new GetActiveWindow, and you are returned the handle of the current window.  We’re not too concerned with the format or type of the value that Windows returns to us here; we just obediently take whatever it is and send it on its merry way into the structure.  This is an example of how we get a value from one DLL call and use it in another.  Now back to our translation methods:

!FileDialogInterface methodsFor: 'translation'!

customFilterIndexTranslated
	^0!

customFilterTranslated
	^String new copyToHeap!

defaultExtentionTranslated
	^self defaultExtention isNil
		ifTrue: [ String new copyToHeap ]
		ifFalse: [ self defaultExtention copyToHeap ]!

fileExtensionTranslated
	^0!

fileFilterTranslated
	"The fileFilter ivar contains an OrderedCollection of associations.
		DLLCC needs a single string, with each section null-terminated.
		A double null signals the end. Build and return it."
	| str null |
	self fileFilter isNil ifTrue: [ ^String new copyToHeap] .
	null := Character value: 0 .
	str := WriteStream on: String new .
	fileFilter do: [ :anAssociation |
		str nextPutAll: anAssociation key ;
			nextPut: null ;
			nextPutAll: anAssociation value ;
			nextPut: null ] .
	str nextPut: null .
	^str contents copyToHeap!

fileNameTranslated
	| buf |
	buf := String new: 300 .
	self fileName isNil
		ifTrue: [ buf at: 1 put: (Character value: 0) ]
		ifFalse:[ buf replaceFrom: 1 to: self fileName size with: self fileName ].
	^buf copyToHeap!

fileOffsetExtensionTranslated
	^0!

fileOffsetTranslated
	^0!

fileTitleTranslated
	^(String new: 100) copyToHeap!

filterIndexTranslated
	self fileFilter isNil ifTrue: [ ^0 ] .
	self filterIndex isNil ifTrue: [ filterIndex := 0 ] .
	^self filterIndex > self fileFilter size
		ifTrue: [ 0 min: self fileFilter size ]
		ifFalse: [ self filterIndex ]!

initialDirectoryTranslated
	^self initialDirectory isNil
		ifTrue: [ String new copyToHeap ]
		ifFalse: [ self initialDirectory copyToHeap ]!

maxFileNameTranslated
	^300!

maxFileTitleTranslated
	^100!

optionsTranslatedFrom: anInterfaceClass
	| lookup cntr |
	lookup := self flagTranslationDictionaryFrom: anInterfaceClass .
	cntr := 0 . 
	self options keysAndValuesDo: 
		[ :k :v |
			v ifTrue: [ cntr := cntr + (lookup at: k) ] ] .
	^cntr!

startingDirectoryTranslated
	^self startingDirectory!

structureSizeTranslated
	^76!

titleBarTextTranslated
	^self titleBarText isNil
		ifTrue: [ String new copyToHeap ]
		ifFalse: [ self titleBarText copyToHeap ]!

windowOwnerTranslated
	^WindowsUserDLL new GetActiveWindow! !

Now all we need are the methods to invoke:

!FileDialogInterface methodsFor: 'running'!

freeAllMemoryIn: aStruct
	| oc |
	oc := #( #lpstrFilter #lpstrCustomFilter #lpstrFile #lpstrFileTitle #lpstrInitialDir #lpstrTitle #lpstrDefExt) .
	oc do: [ :each | (aStruct memberAt: each) freePointer ] .
	aStruct freePointer!

openFileDialog
	| struct class res |
	class := Win32CommonDialogs new .
	struct := self structureIn: class .
	res := class GetOpenFileName: struct .
	res = 1 ifTrue: [returnedFilePath := (struct memberAt: #lpstrFile) copyCStringFromHeap] .
	self freeAllMemoryIn: struct .
	^res = 1!

saveFileDialog
	| struct class res |
	class := Win32CommonDialogs new .
	struct := self structureIn: class .
	res := class GetSaveFileName: struct .
	res = 1 ifTrue: [returnedFilePath := (struct memberAt: #lpstrFile) copyCStringFromHeap] . 
	self freeAllMemoryIn: struct .
	^res = 1! !

!FileDialogInterface methodsFor: 'private'!

flagTranslationDictionaryFrom: anInterfaceClass
	^IdentityDictionary new
		at: #allowMultiSelect put: anInterfaceClass OFN_ALLOWMULTISELECT ;
		at: #createPrompt put: anInterfaceClass OFN_CREATEPROMPT ;
		at: #enableHook put: anInterfaceClass OFN_ENABLEHOOK ;
		at: #enableTemplate put: anInterfaceClass OFN_ENABLETEMPLATE ;
		at: #enableTemplateHandle put: anInterfaceClass OFN_ENABLETEMPLATEHANDLE ;
		at: #extensionDifferent put: anInterfaceClass OFN_EXTENSIONDIFFERENT ;
		at: #fileMustExist put: anInterfaceClass OFN_FILEMUSTEXIST ;
		at: #hideReadonly put: anInterfaceClass OFN_HIDEREADONLY ;
		at: #noChangeDir put: anInterfaceClass OFN_NOCHANGEDIR ;
		at: #noReadonlyReturn put: anInterfaceClass OFN_NOREADONLYRETURN ;
		at: #noTestFileCreate put: anInterfaceClass OFN_NOTESTFILECREATE ;
		at: #noValidate put: anInterfaceClass OFN_NOVALIDATE ;
		at: #overwritePrompt put: anInterfaceClass OFN_OVERWRITEPROMPT ;
		at: #readonly put: anInterfaceClass OFN_READONLY ;
		at: #shareAware put: anInterfaceClass OFN_SHAREAWARE ;
		at: #shareFallThrough put: anInterfaceClass OFN_SHAREFALLTHROUGH ;
		at: #shareNoWarn put: anInterfaceClass OFN_SHARENOWARN ;
		at: #shareWarn put: anInterfaceClass OFN_SHAREWARN ;
		at: #showHelp put: anInterfaceClass OFN_SHOWHELP ;
		yourself!

structureIn: interfaceClass
	| struct |
	struct := interfaceClass tagOFN malloc .
	struct memberAt: #Flags put: (self optionsTranslatedFrom: interfaceClass) .
	struct memberAt: #hwndOwner put: self windowOwnerTranslated .
	struct memberAt: #lStructSize put: self structureSizeTranslated .
	struct memberAt: #lpstrFilter put: self fileFilterTranslated .
	struct memberAt: #lpstrCustomFilter put: self customFilterTranslated .
	struct memberAt: #nMaxCustFilter put: self customFilterIndexTranslated .
	struct memberAt: #nFilterIndex put: self filterIndexTranslated .
	struct memberAt: #lpstrFile put: self fileNameTranslated .
	struct memberAt: #nMaxFile put: self maxFileNameTranslated .
	struct memberAt: #lpstrFileTitle put: self fileTitleTranslated .
	struct memberAt: #nMaxFileTitle put: self maxFileTitleTranslated .
	struct memberAt: #lpstrInitialDir put: self initialDirectoryTranslated .
	struct memberAt: #lpstrTitle put: self titleBarTextTranslated .
	struct memberAt: #lpstrDefExt put: self defaultExtentionTranslated .
	^struct! !

Now using the file dialog box is reduced to its simplest possible form:
FileDialogInterface new openFileDialog

At this point, we are finished. We have achieved our goal; we can publish our interface and let the Smalltalk programmers in our organization use the File Dialog box without knowing anything about the underlying implementation.
All the code for this article is available in the University of Illinois Smalltalk Archives.  One of the included classes is called FileDialogEditor, a real-world example of using the File Dialog box.  You can use it for reading / modifying / saving text files, as well as to file in Smalltalk code.  The entire implementation is not shown here, as it is standard VisualWorks Smalltalk, but two of the methods must be looked at closely:

readFile
	| fn dl arr |
	dl := FileDialogInterface new .
	arr := Array with: 'Smalltalk Files {*.st}' -> '*.st' 
		with: 'Change Logs {*.cha}' -> '*.cha' 
		with: 'Text Files {*.txt}' -> '*.txt' 
		with: 'All Files {*.*}' -> '*.*' .
	dl fileFilter: arr .

The readFile method starts with the definition of the filters. The box will open showing all Smalltalk files.  Since we’re not defining the filterIndex, the box will open using the default, which is the first filter supplied.  If we wanted to open showing all text files, we’d define a filter index of 3.

	dl addOption: #hideReadonly ;
		addOption: #noChangeDir ;
		addOption: #fileMustExist .

We want to hide the readonly check box, and we do not want to change the current directory. I recommend always specifying noChangeDir unless you have a good reason not to. The current directory is the location for your change log and image file, unless you have explicitly defined a path in the “settings” dialog. If you allow the current directory to change, you might start writing changes to a new change log that VisualWorks will obediently create in the new current directory.  #fileMustExist precludes the user from typing in the name of a nonexistent file; only files that really exist can be specified.
	dl openFileDialog ifFalse: [ ^nil ] .
If the dialog box returns false, it was unsuccessful or the user pressed the cancel button.
	fn := dl returnedFilePath .
fn will contain the string identifying the file that was selected.  Remember that it’s just a string, and we still have to make a Filename object in order to read anything:
	self filename value: fn asFilename .
	Cursor read showWhile:
		[self uiText value: (filename value contentsOfEntireFile) ] .!

Now the saveFileAs method:

saveFileAs
	| fn dl arr str |
	dl := FileDialogInterface new .
	arr := Array with: 'Text Files {*.txt}' -> '*.txt' 
		with: 'All Files {*.*}' -> '*.*' .
	dl fileFilter: arr .
	self filename value size > 0
	ifTrue: [ dl initialDirectory: self filename value head .
		dl fileName: self filename value tail ] .

If we read a file, we have stored the current filename in the filename ivar; the message head retrieves the directory and the message tail retrieves the name of the file.  Let’s start at the directory from which we retrieved the file, and load the name of that file as the default, letting the user change it if he wants to.  If we’re creating a file from the keyboard without reading a file first, the filename ivar will be nil, so we can’t seed the filename and starting directory.
	dl addOption: #hideReadonly ;
		addOption: #noChangeDir .
The same options are in effect as for the readFile method, except we removed the fileMustExist option. We must permit the File Dialog box to create a new file if that’s what the user wants to do.

	dl saveFileDialog ifFalse: [ ^nil ] .
	fn := dl returnedFilePath asFilename .
	self filename value: fn .
	str := fn writeStream .
	Cursor write showWhile:
		[ str nextPutAll: self uiText value ] .
	str close! !
The same philosophy exists here as was in the readFile method: call the appropriate interface method, and if it returns false, quit. Otherwise, get the returned file name string, convert it into a Filename object, and display the appropriate cursor while processing the file.

Bibliography
Maruzzi, Stefano – The Microsoft Windows 95 Developer’s Guide – Ziff-Davis Press, Emeryville, California - 1996
Simon, Richard – Windows 95 Common Controls & Messages API Bible – The Waite Group Press, Corte Madera, California – 1996
VisualWorks DLL and C Connect User’s Guide rev. 1.0 – ParcPlace Systems, Sunnyvale, California – 1994 
Header files copied from Microsoft Visual C++ version 4.0