rubywindowswinapifiddle

Native file open dialog in Windows Ruby


As far as I understand, there are no gems which help to open native file dialogs, so I am interested in writing one, specifically for Windows

I'm stuck at the first step which is getting the CLSID for the file open dialog, I read somewhere that I need to SysAllocString and pass the resulting BSTR to CLSIDFromString

require 'fiddle'
require 'fiddle/import'
require 'fiddle/types'

include Fiddle
include Fiddle::CParser

clsid = "{DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7}\0".encode 'UTF-16' # <= must be WCHAR according to winapi specs
clsidptr = Pointer[clsid]

oleaut_dll = Fiddle.dlopen 'OleAut32'
sysallocstring = Function.new oleaut_dll['SysAllocString'], [parse_ctype('const char* string')], parse_ctype('char* bstr')
bstr = sysallocstring.call(clsid) # <= the string here is 0 length which should not be the case
p bstr.to_s

ole_dll = Fiddle.dlopen 'Ole32'
clsidfromstring = Function.new ole_dll['CLSIDFromString'], [TYPE_VOIDP, TYPE_VOIDP], TYPE_INT

buf = '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
bufptr = Pointer[buf]
value = clsidfromstring.call(clsidptr, bufptr)

begin
  if value == -2_147_221_005
    raise 'CO_E_CLASSSTRING'
  elsif value == -2_147_024_808
    raise 'E_INVALIDARG'
  else
    puts 'NOERROR'
    puts buf
  end
ensure
  sysfreestring = Function.new oleaut_dll['SysFreeString'], [parse_ctype('char* string')], parse_ctype('void')
  sysfreestring.call(bstr)
end

However I've only been getting CO_E_CLASSSTRING, I've tried different encodings and have not found any solution.

Any and all help would be appreciated

To people saying make a GUI in tk, I would prefer to use native dialog boxes


Solution

  • Using ffi, comdlg32.dll, and GetOpenFileName this seems to work and doesn't require OLE:

    # Gems needed: ffi, ffi_wide_char
    
    require 'ffi'
    require 'ffi_wide_char'
    
    module ComdlgAPI
      extend FFI::Library
      ffi_lib 'comdlg32'
      ffi_convention :stdcall
    
      class OPENFILENAME < FFI::Struct
        layout :lStructSize,       :ulong,
               :hwndOwner,         :pointer,
               :hInstance,         :pointer,
               :lpstrFilter,       :pointer,
               :lpstrCustomFilter, :pointer,
               :nMaxCustFilter,    :ulong,
               :nFilterIndex,      :ulong,
               :lpstrFile,         :pointer,
               :nMaxFile,          :ulong,
               :lpstrFileTitle,    :pointer,
               :nMaxFileTitle,     :ulong,
               :lpstrInitialDir,   :pointer,
               :lpstrTitle,        :pointer,
               :Flags,             :ulong,
               :nFileOffset,       :ushort,
               :nFileExtension,    :ushort,
               :lpstrDefExt,       :pointer,
               :lCustData,         :pointer,
               :lpfnHook,          :pointer,
               :lpTemplateName,    :pointer,
               :pvReserved,        :pointer,
               :dwReserved,        :ulong,
               :FlagsEx,           :ulong
      end
    
      attach_function :GetOpenFileNameW, [OPENFILENAME.by_ref], :bool
    end
    
    def open_file_dialog
      ofn = ComdlgAPI::OPENFILENAME.new
      ofn[:lStructSize] = ComdlgAPI::OPENFILENAME.size
      ofn[:lpstrFile]   = FFI::MemoryPointer.new(:char, 260 * 2)
      ofn[:nMaxFile]    = 260
      
      # Use UTF-16LE encoding for wide strings
      filters = "All Files\0*.*\0Text Files\0*.TXT\0\0".encode('UTF-16LE')
      ofn[:lpstrFilter] = FFI::MemoryPointer.from_string(filters)
      
      ofn[:nFilterIndex] = 1
      ofn[:Flags] = 0x00000800 | 0x00001000  # OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST
    
      if ComdlgAPI.GetOpenFileNameW(ofn)
        # Use ffi_wide_char helper to convert
        # memory to UTF-16LE, and then encode as UTF-8
        return FfiWideChar.read_wide_string(ofn[:lpstrFile]).encode('UTF-8')
      else
        return nil
      end
    end
    
    if file_path = open_file_dialog
      puts "Selected file: #{file_path}"
    else
      puts "No file selected"
    end