Saturday, June 2, 2007

Displaying a MessageBox Using the Windows API

Sometimes the only user interface you need is a message box. Maybe you just need to alert the user to the completion of a process; or maybe you need to ask a question to which the user responds Yes or No. You'd rather not resort to a console window; but neither do you want to load an entire GUI library to display a simple message box. "Dave", you say, "there must be a better way."

Indeed, there is. Use the DL library to call the MessageBoxA Windows API function.

First, require the dl library:


require 'dl'

We'll feed the function our message text, a dialog box title, and an integer that defines what buttons to display. Let's define some meaningful constants to represent the possible button values:

BUTTONS_OK = 0
BUTTONS_OKCANCEL = 1
BUTTONS_ABORTRETRYIGNORE = 2
BUTTONS_YESNO = 4

The function will return an integer representing the button that the user clicked, so let's define some meaningful constants to represent the possible return code values:

CLICKED_OK = 1
CLICKED_CANCEL = 2
CLICKED_ABORT = 3
CLICKED_RETRY = 4
CLICKED_IGNORE = 5
CLICKED_YES = 6
CLICKED_NO = 7

Here's our method to call the MessageBoxA function from the user32 DLL:

def message_box(txt='', title='', buttons=0)
user32 = DL.dlopen('user32')
msgbox = user32['MessageBoxA', 'ILSSI']
r, rs = msgbox.call(0, txt, title, buttons)
return r
end

Here's how we call it display a message box with an OK button:

message_box("Hello, World!", "Hi!", BUTTONS_OK)

Here's how we call it to display a message box with 'Yes' and 'No' buttons, and process the response:

response = message_box("Are you sure you want to proceed?", "Proceed?", BUTTONS_YESNO)
if response == CLICKED_YES
# insert your code here
end

Finally, here it is in its entirety:

require 'dl'

# button constants
BUTTONS_OK = 0
BUTTONS_OKCANCEL = 1
BUTTONS_ABORTRETRYIGNORE = 2
BUTTONS_YESNO = 4

# return code constants
CLICKED_OK = 1
CLICKED_CANCEL = 2
CLICKED_ABORT = 3
CLICKED_RETRY = 4
CLICKED_IGNORE = 5
CLICKED_YES = 6
CLICKED_NO = 7

def message_box(txt, title=APP_TITLE, buttons=BUTTONS_OK)
user32 = DL.dlopen('user32')
msgbox = user32['MessageBoxA', 'ILSSI']
r, rs = msgbox.call(0, txt, title, buttons)
return r
end

response = message_box("Are you sure you want to proceed?", "Proceed?", BUTTONS_YESNO)
if response == CLICKED_YES
# insert your code here
end

There you have it. As always, let me know if you have questions, comments, or suggestions.

Thanks for stopping by!

Digg my article

16 comments:

Unknown said...

Hi David,
excellent blog, very interesting. That's stuff you don't find easily elsewhere. I'm glad I stepped by.

Congratulations and keep on!
Marco

David Mullet said...

Thanks, Marco!

"That's stuff you don't find easily elsewhere. I'm glad I stepped by."

Then I have done my job. I want this blog to be a valuable resource for Ruby developers, and to demonstrate the immense value that Ruby offers for the Windows platform.

David

Anonymous said...

Yo David!!
Excellent... You've given me a path to a solution to what I was trying to do. Keep'em coming.

Thanks a lot

Vijay

Anonymous said...

Anyone know how to monitor Mass Storage Insert events (eg. When someone inserts a USB drive) from Ruby?

Anonymous said...

@Anko: You probably won't get a very detailed account, but just know that much of that stuff that is very `Windowsy' will require you to use something like Win32 OLE (dave has written up something here).

In particular, if you did some stuff in those schweet days of Classic ASP, that stuff can almost all happen in Ruby with Win32 OLE stuff. Like sending mail, et cetera. So, if you find a solution based on Win32 OLE, you can translate well. I think.

Plus, thanks, Dave, for this one. I am a bit too Unix, for one, and Win32 is scary territory, but I guess it will hurt less with you around. :-)

David Mullet said...

@Anko:

I have just posted a new article related to your question here. I hope that helps.

David

Anonymous said...

Hi David, I have never done any windows programming, so I have no prior knowledge of windows dll's. Any sites I have found for the Win API is for C/C++, which I have no experience in. Do you have any suggestion for sites to go to or books to get that will explain all the dll's available and how to use them in Ruby?

Anonymous said...

Great, just what I needed.

Only one disadvantage: there's no icon on the message box. To use the Windows standard icons add the following code:

--------8<--------------
ICON_HAND = 16
ICON_QUESTION = 32
ICON_EXCLAMATION = 48
ICON_ASTERISK = 64

def message_box(txt='', title='', buttons=0, icon=0)
user32 = DL.dlopen('user32')
msgbox = user32['MessageBoxA', 'ILSSI']
r, rs = msgbox.call(0, txt, title, buttons+icon)
return r
end
--------8<--------------

Use it like that:
response = message_box("Are you sure you want to proceed?", "Proceed?", BUTTONS_YESNO, ICON_QUESTION)

Anonymous said...

Great stuff! Please update the site!

Anonymous said...

Something I observe when scripting a spreadsheet program which is *not* MS-Excel. I hope, though, it is the same there:

The method Sheets() of the workbook-object works differently, when parameterized with a String, that starts with a "cypher", i.e.
'02_2008'. I am unable to activate a sheet with such a name until I prefix it with another character, like this: 'M02_2008'.

The same works flawlessly in VB. I cannot but assume, that something special to Win32OLE is responsible.
TY anyway. ;-)

Anonymous said...

Hi, thanks for the excellent info.

One problem I have though is that the dialog box can quite easily get lost, simply because the user is doing something else while the script is running the background and their window takes focus over the dialog.

Do you know of a way to make thie Ruby dialog have properties such as always on top etc.?

I know in the Win32 API there is a owner window, so could it possible be hooked into that?

One other thought is that I am calling the script from a C# application. Is it possible to use the Window Handle of that in the Ruby script?

Thanks
Lee

Anonymous said...

Excellent post! I've been looking for a simple way to do this. Never thought of using the windows API. Glad I found this!

Bobby Washington said...

Thank you sooooo much for this code snippet. It was a simple cut an paste for me!!! If you don't mind me asking, how did you know about this library? It is italized on the Ruby Standard Library Documentation meaning it is not documented. This is a skill I would like to have.

Thank you,
Bobby Washington

Christian Baumann said...

For Ruby 1.9 this produces an error:

message_box.rb:20:in `[]': wrong number of arguments(2 for 1) (ArgumentError)
from message_box.rb:20:in `message_box'
from message_box.rb:25:in `'

The solution is to replace lines 20 & 21 by:

msgbox = DL::CFunc.new(user32['MessageBoxA'], DL::TYPE_LONG, 'MessageBox')
r, rs = msgbox.call([0, txt, title, 0].pack('L!ppL!').unpack('L!*'))

Source: http://www.ruby-forum.com/topic/138277

Anonymous said...

OK
so i'm really new to ruby here

stand on the shoulders of giants

but i'd like to suggest:

r, rs = msgbox.call([0, txt, title, buttons].pack('L!ppL!').unpack('L!*'))

instead....the "buttons" variable was accidentally left as 0

Anonymous said...

there is a gem for this:
http://www.rubydoc.info/gems/messagebox/0.1.0