An olde worlde open file dialog

Windows specific questions.
Josep Roca
Posts: 564
Joined: Sep 27, 2016 18:20
Location: Valencia, Spain

Re: An olde worlde open file dialog

Post by Josep Roca »

Solved!

Instead of trying to set the path using the wszInitialDir parameter, do it using the wszFile parameter, i.e. instead of DIM wszFile AS WSTRING * 260 = "*.*" use DIM wszFile AS WSTRING * 260 = AfxGetExePath & "\*.*"

Code: Select all

#define UNICODE
#INCLUDE ONCE "Afx/CWindow.inc"
USING Afx

CONST IDC_OFD = 1001

DECLARE FUNCTION WinMain (BYVAL hInstance AS HINSTANCE, _
                          BYVAL hPrevInstance AS HINSTANCE, _
                          BYVAL szCmdLine AS ZSTRING PTR, _
                          BYVAL nCmdShow AS LONG) AS LONG

   END WinMain(GetModuleHandleW(NULL), NULL, COMMAND(), SW_NORMAL)

' // Forward declarations
DECLARE FUNCTION WndProc (BYVAL hwnd AS HWND, BYVAL uMsg AS UINT, BYVAL wParam AS WPARAM, BYVAL lParam AS LPARAM) AS LRESULT

' ========================================================================================
' Main
' ========================================================================================
FUNCTION WinMain (BYVAL hInstance AS HINSTANCE, _
                  BYVAL hPrevInstance AS HINSTANCE, _
                  BYVAL szCmdLine AS ZSTRING PTR, _
                  BYVAL nCmdShow AS LONG) AS LONG

   ' // Set process DPI aware
   AfxSetProcessDPIAware

   ' // Create the main window
   DIM pWindow AS CWindow
   pWindow.Create(NULL, "AfxOpenFileDialog example", @WndProc)
   pWindow.SetClientSize(500, 320)
   pWindow.Center

   ' // Add a button
   pWindow.AddControl("Button", , IDC_OFD, "&Open File Dialog", 350, 250, 110, 23)

   

   ' // Dispatch messages
   FUNCTION = pWindow.DoEvents(nCmdShow)

END FUNCTION
' ========================================================================================

' ========================================================================================
' Main window callback procedure
' ========================================================================================
FUNCTION WndProc (BYVAL hwnd AS HWND, BYVAL uMsg AS UINT, BYVAL wParam AS WPARAM, BYVAL lParam AS LPARAM) AS LRESULT

   SELECT CASE uMsg

      CASE WM_COMMAND
         SELECT CASE GET_WM_COMMAND_ID(wParam, lParam)
            ' // If ESC key pressed, close the application sending an WM_CLOSE message
            CASE IDCANCEL
               IF GET_WM_COMMAND_CMD(wParam, lParam) = BN_CLICKED THEN
                  SendMessageW hwnd, WM_CLOSE, 0, 0
                  EXIT FUNCTION
               END IF
            ' // Display the Open File Dialog
            CASE IDC_OFD
               IF GET_WM_COMMAND_CMD(wParam, lParam) = BN_CLICKED THEN
                  DIM wszFile AS WSTRING * 260 = AfxGetExePath & "\*.*"
                  DIM wszFilter AS WSTRING * 260 = "BAS files (*.BAS)|*.BAS|" & "All Files (*.*)|*.*|"
                  DIM dwFlags AS DWORD = OFN_FILEMUSTEXIST OR OFN_HIDEREADONLY
                  DIM cws AS CWSTR = AfxOpenFileDialog(hwnd, "", wszFile, "", wszFilter, "BAS", @dwFlags, NULL)
                  MessageBoxW(hwnd, cws, "File", MB_OK)
                  EXIT FUNCTION
               END IF
         END SELECT

    	CASE WM_DESTROY
         ' // Quit the application
         PostQuitMessage(0)
         EXIT FUNCTION

   END SELECT

   ' // Default processing of Windows messages
   FUNCTION = DefWindowProcW(hWnd, uMsg, wParam, lParam)

END FUNCTION
' ========================================================================================
Windows 7:

- If lpstrInitialDir has the same value as was passed the first time the application used an Open or Save As dialog box, the path most recently selected by the user is used as the initial directory.
- Otherwise, if lpstrFile contains a path, that path is the initial directory.
- Otherwise, if lpstrInitialDir is not NULL, it specifies the initial directory.
- If lpstrInitialDir is NULL and the current directory contains any files of the specified filter types, the initial directory is the current directory.
- Otherwise, the initial directory is the personal files directory of the current user.
- Otherwise, the initial directory is the Desktop folder.
jj2007
Posts: 2326
Joined: Oct 23, 2016 15:28
Location: Roma, Italia
Contact:

Re: An olde worlde open file dialog

Post by jj2007 »

And Microsoft even gives you an explanation why they changed the behaviour:
Historically lpstrInitialDir would override "most recently used" folder. In general, this was not a good or expected user experience because the dialog did not remember the last place where the user saved, which is usually, the place where they want to save next time. The new API (IFileDialog) allows applications to ask for a default folder (IFileDialog::SetDefaultFolder) which is prefferred for most cases, or explicitly set the folder (IFileDialog::SetFolder) which should be sparingly used in select cases.

In Windows 7 we updated the legacy API beahvior (GetOpenFileName) to match the preferred behavior since “the file dialog forgot where I saved” was was a significant source of negative feedback in Windows Vista.
Spelling errors are Microsoft, not mine ;-)
Josep Roca
Posts: 564
Joined: Sep 27, 2016 18:20
Location: Valencia, Spain

Re: An olde worlde open file dialog

Post by Josep Roca »

Yes, I remember that when I was using Windows 2000 I had to write a wrapper with a static variable that stored the last path used and used this value to set the directory when the dialog was called again.
deltarho[1859]
Posts: 4313
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: An olde worlde open file dialog

Post by deltarho[1859] »

José, it looks like you have nailed it.

In one instance, of quite a few, I am using

Code: Select all

wszFile = AfxGetExePath & "\*RSAPublicKey*.dat"
wszFilter = "*RSAPublicKey*.dat|*RSAPublicKey*.dat|"
wszInitialDir = ""
It may seem an odd filter but we can have AliceRSAPublicKey.dat, BobRSAPublicKey.dat and so on or we could use an append protocol instead.

I'll use wszInitialDir = "" so that I can understand the code if I read it again the future.

The "Quantum entanglement" I mentioned earlier has now ceased.

Needless to say I will give this approach a hammering and report back in any event.

Thanks José.
deltarho[1859]
Posts: 4313
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: An olde worlde open file dialog

Post by deltarho[1859] »

Yours truly wrote:I then put OFDHook into two folders. After using OFDHook in one folder I then moved to the other folder and that is where things went wrong.
Yep, that is no longer true. <smile>
jj2007
Posts: 2326
Joined: Oct 23, 2016 15:28
Location: Roma, Italia
Contact:

Re: An olde worlde open file dialog

Post by jj2007 »

Josep Roca wrote:Yes, I remember that when I was using Windows 2000 I had to write a wrapper with a static variable that stored the last path used and used this value to set the directory when the dialog was called again.
I still use that (successfully) in the CDN_FOLDERCHANGE handler, also because I cannot confirm this behaviour on Win7-64:
If lpstrInitialDir has the same value as was passed the first time the application used an Open or Save As dialog box, the path most recently selected by the user is used as the initial directory
On the second call to GetOpenFileName, I don't get the "most recently selected" path but rather the initially set path.
dodicat
Posts: 7983
Joined: Jan 10, 2006 20:30
Location: Scotland

Re: An olde worlde open file dialog

Post by dodicat »

If I set lpstrInitialDir to a fake then the previous path comes up at a new run

Code: Select all


#define WIN_INCLUDEALL
#Include Once "windows.bi"
#Include once "/win/commctrl.bi"
#include "file.bi"
Sub getfiles(Byref File As OpenFileName,flag As String)
    Dim As zstring * 2048 SELFILE
    Dim As String MYFILTER=flag+Chr(0)
    With File
  .lStructSize = sizeof(OpenFileName)
  .hwndOwner = null 
  .hInstance = null 
  .lpstrFilter = strptr(MYFILTER)
  .nFilterIndex = 0
  .lpstrFile = @SELFILE
  .nMaxFile = 2048 
  .lpstrFileTitle = null 
  .nMaxFileTitle = 0 
  .lpstrInitialDir = @"nosuch:\"
  .lpstrTitle = @"Open"
  .Flags = 4096
  .nFileOffset = 0 
  .nFileExtension = 0 
  .lpstrDefExt = null 
    End With
    GetOpenFileName(@File)
End Sub

dim file As OpenFileName
dim as string myfilter
myfilter = "All Files"+chr(0)         +"*.*"+chr(0)
myfilter += "FreeBasic Files"+chr(0)  +"*.bas;*.bi"+chr(0)
myfilter+="C and C++ files"+chr(0)    +"*.c;*.c++;*.cpp;*.h"+chr(0)


do
 getfiles(file,myfilter)
               var s1=*file.lpstrFile
               print s1
               print "length of file ";filelen(s1)
               print "Press a key for more, esc to end"
               sleep
               loop until inkey=chr(27)  
Win 10.
However, a proper olde worlde thing would be dosshell
deltarho[1859]
Posts: 4313
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: An olde worlde open file dialog

Post by deltarho[1859] »

According to the OPENFILENAME structure we have two platforms for determining the initial directory: Windows 2000/XP/Vista and Windows 7.

From jj2007's link we have David Washington at Microsoft referring to "negative feedback in Windows Vista". Why? It was like that with Win 2000 and XP. Well, certainly with Win 2000 according to José's comments.

I am on Windows 10 using the Windows 7 method.

jj2007 is on Win7-64 which seems to be using Vista's method.

I am now losing the will to live and need to lie down in a darkened room.
dodicat wrote:If I set lpstrInitialDir to a fake then the previous path comes up at a new run
I think that follows from the Win7 method which Win10 uses.

Anyway, the upshot of this thread is that I can use whichever method takes my fancy; the path Microsoft should have taken instead of deciding which side of the fence to be on.
dodicat
Posts: 7983
Joined: Jan 10, 2006 20:30
Location: Scotland

Re: An olde worlde open file dialog

Post by dodicat »

Just start at the beginning.

Code: Select all


shell "start explorer ,"  
deltarho[1859]
Posts: 4313
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: An olde worlde open file dialog

Post by deltarho[1859] »

I have another problem now.<Grrrr>

A friend of mine has been testing my latest project and before sending him the latest version I created a folder and included a couple of exes, one of which creates four key blobs. I then did exactly what he needs to do so as to make sure every works as planned..

To my horror BCryptImportKeyPair went down on an error trap. The key blob generator was last modified at the beginning of April and I have used it stacks of times without issue.

*RSAPublicKey*.dat has been working perfectly in wszInitialDir since I introduced it. However, something goes wrong in wszFile, José's latest idea. It works with AliceRSAPublicKey.dat, BobRSAPublicKey.dat and so on but it fails with RSAPublickey.dat ie without a prepended name. The filename returned is "F:\Chris\RSAPublicKey.dat,t" with ',t' added. I use the filename, shortly after, in 'Open <Whatever>, for Binary as #f'. What I am not doing is to check that a file name exists when it is selected from an open file dialog - who the hell would? Obviously, the key blob did not get loaded and the app fell over.

If I changed RSAPublicKey.dat to MyRSAPublickey.dat then all is well. A "*.*" filter works OK, BTW.

So, when we use the wszFile approach we need to be careful with the filtering. This is clearly a bug in GetOpenFilenameW.
jj2007
Posts: 2326
Joined: Oct 23, 2016 15:28
Location: Roma, Italia
Contact:

Re: An olde worlde open file dialog

Post by jj2007 »

deltarho[1859] wrote:jj2007 is on Win7-64 which seems to be using Vista's method.
I am normally on Win7, but I have a WinXP VM and a Win10 machine, too, so I made some tests:

1. with lpstrInitialDir pointing to a nullstring, it keeps the last selected folder even if the app is restarted (!). And that folder is not curdir; when starting the program, curdir is identical to the exe's folder

2. with lpstrInitialDir pointing to "C:\somefolder", it starts with this folder, but if the user changes the folder, a second call uses the new folder; when restarting the program, it goes to "C:\somefolder" again.

No idea how Windows manages 1. - I searched the registry for the name (without extension) of the exe, hoping to find an entry with the last selected folder, no luck. This behaviour is very odd - how does Windows remember that folder?

Re 2., setting lpstrInitialDir to a precise path is definitely a more reliable way to ensure that you see what you want to see. Windows is messy, as usual...

Oh, before I forget this little detail: This behaviour is identical on WinXP, Win7 and Win10.
Josep Roca
Posts: 564
Joined: Sep 27, 2016 18:20
Location: Valencia, Spain

Re: An olde worlde open file dialog

Post by Josep Roca »

> So, when we use the wszFile approach we need to be careful with the filtering. This is clearly a bug in GetOpenFilenameW.

No, it is a bug in the AfxOpenFileDialog function. Replace it with the one below one and it will work (hopefully).

Code: Select all

PRIVATE FUNCTION AfxOpenFileDialog ( _
   BYVAL hwndOwner AS HWND _                    ' // Parent window
 , BYREF wszTitle AS WSTRING _                  ' // Caption
 , BYREF wszFile AS WSTRING _                   ' // Filename
 , BYREF wszInitialDir AS WSTRING _             ' // Start directory
 , BYREF wszFilter AS WSTRING _                 ' // Filename filter
 , BYREF wszDefExt AS WSTRING _                 ' // Default extension
 , BYVAL pdwFlags AS DWORD PTR = NULL _         ' // Flags
 , BYVAL pdwBufLen AS DWORD PTR = NULL _        ' // Buffer length
 ) AS CWSTR

   DIM dwFlags AS DWORD, dwBufLen AS DWORD
   IF pdwFlags THEN dwFlags = *pdwFlags
   IF pdwBufLen THEN dwBufLen = *pdwBuflen

   ' // Filter is a sequence of WSTRINGs with a final (extra) double null terminator
   ' // The "|" characters are replaced with nulls
   DIM wszMarkers AS WSTRING * 4 = "||"
   IF RIGHT(wszFilter, 1) <> "|" THEN wszMarkers += "|"
   DIM cwsFilter AS CWSTR = wszFilter & wszMarkers
   DIM dwFilterStrSize AS DWORD = LEN(cwsFilter)
   ' // Replace markers("|") with nulls
   DIM pchar AS WCHAR PTR = *cwsFilter
   FOR i AS LONG = 0 TO LEN(cwsFilter) - 1
      IF pchar[i] = ASC("|") THEN pchar[i] = 0
   NEXT

   ' // If the initial directory has not been specified, assume the current directory
   ' // FreeBasic 64 bit fails if we pass an empty string and we try to modify wszInitialDir
   DIM _wszInitialDir AS WSTRING * MAX_PATH = wszInitialDir
   IF LEN(_wszInitialDir) = 0 THEN _wszInitialDir = CURDIR
   ' // The size of the buffer must be at least MAX_PATH characters
   IF dwBufLen = 0 THEN
      IF (dwFlags AND OFN_ALLOWMULTISELECT) = OFN_ALLOWMULTISELECT THEN dwBufLen = 32768  ' // 64 Kb buffer
   END IF
   IF dwBufLen < 260 THEN dwBufLen = 260   ' // Make room for at least one path
   ' // Allocate the file name and a marker ("|") to be replaced with a null
   DIM cwsFile AS CWSTR = wszFile & "|"
   ' // Store the position of the marker
   DIM cbPos AS LONG = LEN(cwsFile) - 1
   ' // Allocate room for the buffer
   IF LEN(cwsFile) < dwBufLen THEN cwsFile += SPACE(dwBufLen - LEN(cwsFile))
   DIM dwFileStrSize AS DWORD = LEN(cwsFile)
   ' // The filename must be null terminated (replace the marker with a null)
   pchar = *cwsFile
   pchar[cbPos] = 0

   ' // Fill the members of the structure
   DIM ofn AS OPENFILENAMEW
   ofn.lStructSize     = SIZEOF(ofn)
   IF AfxWindowsVersion < 5 THEN ofn.lStructSize = 76
   ofn.hwndOwner       = hwndOwner
   ofn.lpstrFilter     = *cwsFilter
   ofn.nFilterIndex    = 1
   ofn.lpstrFile       = *cwsFile
   ofn.nMaxFile        = dwFileStrSize
   ofn.lpstrInitialDir = @_wszInitialDir
   IF LEN(wszTitle) THEN ofn.lpstrTitle = @wszTitle
   ofn.Flags = dwFlags OR OFN_EXPLORER
   IF LEN(wszDefExt) THEN ofn.lpstrDefExt = @wszDefExt

   ' // Call the open file dialog
   IF GetOpenFilenameW(@ofn) THEN
      IF (dwFlags AND OFN_ALLOWMULTISELECT) = OFN_ALLOWMULTISELECT THEN
         pchar = *cwsFile
         FOR i AS LONG = 0 TO dwFileStrSize - 1
            ' // If double null, exit
            IF pchar[i] = 0 AND pchar[i + 1] = 0 THEN EXIT FOR
            ' // Replace null with ","
            IF pchar[i] = 0 THEN pchar[i] = ASC(",")
         NEXT
      END IF
      ' // Trim trailing spaces
      cwsFile = RTRIM(**cwsFile, CHR(32))
      IF RIGHT(**cwsFile, 1) = "," THEN cwsFile = LEFT(**cwsFile, LEN(cwsFile) - 1)
   ELSE
      ' // Buffer too small
      IF CommDlgExtendedError = FNERR_BUFFERTOOSMALL THEN
         dwBufLen = ASC(**cwsFile)
      END IF
      cwsFile = ""
   END IF

   ' // Return the retrieved values
   IF pdwFlags THEN *pdwFlags = ofn.Flags
   IF pdwBufLen THEN *pdwBufLen = dwBufLen
   RETURN cwsFile

END FUNCTION
' ========================================================================================
deltarho[1859]
Posts: 4313
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: An olde worlde open file dialog

Post by deltarho[1859] »

Josep Roca wrote:No, it is a bug in the AfxOpenFileDialog function
José, would you tell me how your last post differs to previous versions.

I am using AfxOpenFileDialogHook but ignoring the references to a hook I cannot see what change/s you have made. I thought I might see a change in how cwsFile is handled but I could not see one.
Josep Roca
Posts: 564
Joined: Sep 27, 2016 18:20
Location: Valencia, Spain

Re: An olde worlde open file dialog

Post by Josep Roca »

I mainly have added IF (dwFlags AND OFN_ALLOWMULTISELECT) = OFN_ALLOWMULTISELECT THEN

Code: Select all

   ' // Call the open file dialog
   IF GetOpenFilenameW(@ofn) THEN
      IF (dwFlags AND OFN_ALLOWMULTISELECT) = OFN_ALLOWMULTISELECT THEN
         pchar = *cwsFile
         FOR i AS LONG = 0 TO dwFileStrSize - 1
            ' // If double null, exit
            IF pchar[i] = 0 AND pchar[i + 1] = 0 THEN EXIT FOR
            ' // Replace null with ","
            IF pchar[i] = 0 THEN pchar[i] = ASC(",")
         NEXT
      END IF
Whitout it, it was searching for a double double null terminator, but this ony happens if you use the flag to allow multiple selection.

BTW, if I'm not wrong, using the wszFile workaround disables the hook. This API function is madening. We have to use null terminated strings and they use nulls as separators! No wonder they have deprecated it and have kept it only for backward compatibility.
deltarho[1859]
Posts: 4313
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: An olde worlde open file dialog

Post by deltarho[1859] »

That has done it! RASPublicKey.dat no longer needs a name prepend.

At some point above we drifted from a hook back to the original. My next question was about to be: So, we no longer need a hook?

Since your last post you added:
BTW, if I'm not wrong, using this workaround, disables the hook
No, it doesn't disable the hook - it simply has not got one now. <smile>

I am now getting the all singing, all dancing latest open file dialog display.

So, what you have done José is something that a lot of people have been screaming for.

In a nutshell:

1. If you want the Windows 7 or later initial directory method then go via lpstrInitialDir.
2. If you want the pre Windows 7 method initial directory then go via lpstrFile.

Except for jj2007, and goodness knows how many others, who have Windows 7 and are getting the pre Windows 7 method. That is weird!
Post Reply