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
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