(同步个人博客 http://sxysxy.org/blogs/68 到csdn
封装!
上一节最后用了30多行ruby代码做了一个窗口出来,看起来很是麻烦,如何像XYGui里面一样,一行就能创建那样的窗口呢?
实际上这里面是有很深的套路的,本节开始,讲述XYGui对创建窗口的封装
先解决一些技术上的问题:
已编译成的机器语言与ruby语言互相调用
没错,你没有看错,机器语言调用ruby语言!
高版本的ruby(准确来说是ruby2.0之后)标准库里面就有了一个叫做Fiddle的东西,这个东西很强啊,相当于Dll库+指针库(包括函数指针)+一堆奇怪的东西。。。打开 /lib/XYGui/xy_window.rb,找到一大段注释起来的代码
=begin
TEMP = []
def wndproc
_self = self
_content = @content
_responder = @responder
proc = Class.new(Fiddle::Closure) do
define_method :call do |hwnd, msg, wparam, lparam|
return _self.callproc(hwnd, msg, wparam, lparam)
case msg
when WM_PAINT then
_responder[:ON_BEGINPAINT].call(_self, nil) if _responder[:ON_BEGINPAINT]
return 0
when WM_DESTROY then
_responder[:ON_DESTROY].call(_self, nil) if _responder[:ON_DESTROY]
return 0
when WM_COMMAND then
event = wparam >> 16 #hiword
id = wparam^(wparam>>16)<<16 #loword
_content[id].responder[:ON_COMMAND].call(_self, {:event => event}) if _content[id] && _content[id].responder[:ON_COMMAND]
return 0
when WM_SIZE then
_responder[:ON_BEFORESIZE].call(_self, {:height => WinAPI.hiword(lparam), :width => WinAPI.loword(lparam)}) if _responder[:ON_BEFORESIZE]
return 0
when WM_KEYDOWN then
_responder[:ON_KEYDOWN].call(_self, {:key => wparam}) if _responder[:ON_KEYDOWN]
when WM_CREATE then
_responder[:ON_CREATE].call(_self, nil) if _responder[:ON_CREATE]
return 0
else
return WinAPI.call("user32", "DefWindowProc", hwnd, msg, wparam, lparam)
end
end
end.new(Fiddle::TYPE_INT, [Fiddle::TYPE_INT]*4)
TEMP << proc
Fiddle::Function.new(proc, [Fiddle::TYPE_INT]*4, Fiddle::TYPE_INT).to_i
end
=end
这段代码是相当黑科技的了(嗯但是这点才只是个开始呢),展开了讲ruby底层的机制要很多篇幅的。proc是个继承Fiddle::Closure的匿名类(而且是类也是对象),约定一个叫做”call”的方法,里面就是要做成回调函数的函数内容。
不如用下面这个小例子来帮助理解学习:
require 'fiddle'
include Fiddle
#------ High Power Alert!
proc = Class.new(Closure) do
define_method :call do |a|
puts "You called me with arg a:#{a}, and I will return 2*a+1"
2*a+1
end
end.new(Fiddle::TYPE_INT, [Fiddle::TYPE_INT])
fptr = Fiddle::Function.new(proc, [Fiddle::TYPE_INT], Fiddle::TYPE_INT)
puts fptr.to_i #See what does output?
puts fptr.call 2 #call with ruby, returns 5
puts fptr.to_i 输出了什么呢?一个看起来很奇怪的数,其实他是一个地址,如果我们用c语言的函数指针去调用这个地址,也会得到You called me with arg a:2, and I will return a*1的输出,并且真的得到了5这个返回值。xy_window.rb里面就是用了这样的方法,制作了窗口过程WndProc函数。(当然现在是被注释掉了,因为后来为了执行效率就改用c语言了),然后把Fiddle::Function.new(proc, [Fiddle::TYPE_INT]*4, Fiddle::TYPE_INT).to_i
传给了窗口类的lpfnWndProc成员,再然后windows就可以回调我的ruby写的”函数”了。
讲点额外的姿势
额外的,搞XYGui不必要的,因为已经有Fiddle了,而且后来窗口过程用c语言写了,自然也没有用上。下面这”额外的姿势”难度会稍高,但是非常有趣。不感兴趣的同学可以跳过。
**不借助**Fiddle等标准库直接提供的方法,如何让windows调用ruby写的函数呢?这里我提供一个方法: 动态生成机器语言,具体做法请看 这里 (这点我还是开一篇新的博客专门讲)
用ruby来做WndProc
所以,稍微修改一下第一节的小程序。这还是一个窗口,不过用我们自己,用ruby写的窗口过程函数来处理消息了。可以看到,这次可以正常点击窗口右上角退出程序了。为了证明我们确实做到了,我还让窗口在接收鼠标右键点击的时候弹出一个MessageBox。
require 'win32api'
TITLE = "Window_Test" #窗口标题
CLASS_NAME = "Window_Test_Class" #窗口类
# API
def callAPI(dll, procname, *arg)
Win32API.new(dll, procname, arg.map{|e| e.is_a?(String)? "p":"L"}.join, "L").call *arg
end
#prepare
hInstance = callAPI("kernel32", "GetModuleHandle", 0)
DefProc = callAPI("kernel32", "GetProcAddress", callAPI("kernel32", "GetModuleHandle", "user32"), "DefWindowProcA")
#wndproc
require 'fiddle'
include Fiddle
proc = Class.new(Closure) do
define_method :call do |hWnd, uMsg, wParam, lParam|
case uMsg
when 2 then #WM_DESTROY 0x0002
callAPI("user32", "PostQuitMessage", 0)
return 0
when 0x0204 then #WM_RBUTTONDOWN 0x0204
callAPI("user32", "MessageBox", 0, "Right Button Clicked!", "Tip", 0)
return 0
else
return callAPI("user32", "DefWindowProc", hWnd, uMsg, wParam, lParam)
end
end
end.new(TYPE_INT, [TYPE_INT]*4)
procAddr = Function.new(proc, [TYPE_INT]*4, TYPE_INT).to_i
#window class
wndclass = [0, procAddr, #lpfnWndProc
0, 0, hInstance,
callAPI("user32", "LoadIcon", hInstance, 32512), #Icon
callAPI("user32", "LoadCursor", 0, 32512), #IDC_ARROW
6, #COLOR_WINDOW_FRAME
0, CLASS_NAME].pack("LLLLLLLLLp")
if callAPI("user32", "RegisterClass", wndclass) == 0
callAPI("user32", "MessageBox", 0, "Window class Error", "Error", 0)
exit
end
hWnd = callAPI("user32", "CreateWindowEx", 0, CLASS_NAME, TITLE,
0xcf0000 | 0x10000000, #WS_OVERLAPPEDWINDOW | WS_VISIBLE
100, 100, 400, 300, 0, 0, hInstance, 0)
msg = "\0"*28 #MSG
while callAPI("user32", "GetMessage", msg, 0, 0, 0) != 0 #here, != 0
callAPI("user32", "TranslateMessage", msg)
callAPI("user32", "DispatchMessage", msg)
end
至此,用ruby语言制作窗口过程的技术问题便得到解决了。如果泥自己像做一个gui库,用ruby来处理窗口过程,那这一节的内容可一定要好好看看。XYGui最开始就是用ruby处理窗口过程的…
下一节我会讲用c语言做这个窗口过程(一般来说,是为了执行效率),并讲解如何把ruby的对象与窗口关联起来,这将是”封装”的最关键一步。