Zachary
Zachary

Reputation: 65

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

Upvotes: 0

Views: 140

Answers (1)

Casper
Casper

Reputation: 34308

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

Upvotes: 1

Related Questions