设为首页收藏本站|繁體中文

Project1

 找回密码
 注册会员
搜索
查看: 12712|回复: 24

[原创发布] [XP/VX] 精确获取窗口句柄(解决部分 API 脚本潜在问题)

 关闭 [复制链接]

Lv1.梦旅人

梦石
0
星屑
61
在线时间
24 小时
注册时间
2008-8-5
帖子
1924
发表于 2009-9-1 03:56:15 | 显示全部楼层 |阅读模式

加入我们,或者,欢迎回来。

您需要 登录 才可以下载或查看,没有帐号?注册会员

x
本帖最后由 紫苏 于 2010-10-4 12:00 编辑

当下 6R 有很多调用 API 的脚本都需要获取 RM 游戏主窗口的句柄,从而进行各种窗口相关的操作。这些脚本无外乎就通过以下三种方法来获取:
  • 通过 GetPrivateProfileString 函数获取 Game.ini 中的窗口标题,然后调用 FindWindow 获取匹配该标题和 RGSS Player 类名的窗口句柄
  • 通过 GetActiveWindow 函数获取活动状态的、且属于调用线程的窗口句柄
  • 通过 GetForegroundWindow 函数获取系统中的前台(拥有焦点的)窗口

然而无论哪种方法都有潜在的问题——
首先是第一种。前天精灵在群里提到了一个关于窗口句柄的问题——如果当窗口的标题随时在变动,那么目前通过 FindWindow 获取窗口的脚本都将无法正确获取窗口句柄(因为传递给 FindWindow 的字符串必须得是板上钉钉的,不可匹配部分字符串),导致整个脚本瘫痪。那么我们自然而然地就会想到,不匹配窗口标题,仅匹配窗口类名可以吗?是的,只要传递一个 NULL 给 FindWindow 的第二个参数,就可以仅匹配窗口类名。但是有没有想过,当你开着多个 RMXP 的游戏窗口,这些窗口的窗口类名都是 RGSS Player,那岂不是其中的任意一个窗口都能匹配?再退一步想,即使我们的窗口标题是固定的,难道我们不能打开多个相同的 Game.exe 应用程序实例?这些相同程序的不同进程所创建的窗口标题和类名都是一样的,其中的任意一个窗口也都能匹配,只要我们在窗口创建后切换到另一个匹配的窗口(FindWindow 在 Z 次序中从高到低搜索窗口,这时刚切换到的窗口在 Z 次序中的排位高于我们刚创建的窗口,所以 FindWindow 第一个找到的是这个刚切换到的窗口),我们最后获取到的句柄就变成了这个匹配的窗口的句柄,而不是我们预期的刚创建的窗口句柄……

然后是第二种。GetActiveWindow 的问题是:当这个函数被调用时,如果调用线程(也就是 RM 的主线程)所创建的 RM 的窗口并不是活动状态的(不在前台),那么它就会返回 0。所以,只要我们在 RM 窗口出现之后,这个函数调用之前立刻切换到其它窗口,使用这个函数的 API 脚本也无法正确获取句柄。

最后是第三种。GetForegroundWindow 获取的是目前系统中处在前台的窗口,它的问题也是一目了然——只要在调用之前切换到其它窗口,那么它获取到的句柄就是你刚切换到的窗口的句柄了……

这几个问题一般是出现在 RM 初始化时获取窗口句柄的情况下,因为在初始化开始到调用 FindWindow 等函数之间,你有时间可以切换到其他窗口。如果是在平时调用,比如截图存档脚本,从你按下“存档”到调用 FindWindow 获取窗口句柄的这个时间间距实在是太短了,普通人的动作没有那么快,能赶在调用之前切换到其他窗口……所以下面这个脚本主要是解决了在 RM 初始化时获取句柄的潜在问题。

其实这个问题我很早就注意到了,但是由于其严重性不高,所以一直懒得提出。正好赶上精灵提出这个问题,觉得严重性猛地提升了不少,于是就把这个脚本发出来了……
  1. #==============================================================================
  2. # ■ Kernel
  3. #------------------------------------------------------------------------------
  4. #  该模块中定义了可供所有类使用的方法。Object 类中包含了该模块。
  5. #==============================================================================
  6. module Kernel
  7.   #--------------------------------------------------------------------------
  8.   # ● 需要的 Windows API 函数
  9.   #--------------------------------------------------------------------------
  10.   GetWindowThreadProcessId = Win32API.new("user32", "GetWindowThreadProcessId", "LP", "L")
  11.   GetWindow = Win32API.new("user32", "GetWindow", "LL", "L")
  12.   GetClassName = Win32API.new("user32", "GetClassName", "LPL", "L")
  13.   GetCurrentThreadId = Win32API.new("kernel32", "GetCurrentThreadId", "V", "L")
  14.   GetForegroundWindow = Win32API.new("user32", "GetForegroundWindow", "V", "L")
  15.   #--------------------------------------------------------------------------
  16.   # ● 获取窗口句柄
  17.   #--------------------------------------------------------------------------
  18.   def get_hWnd
  19.     # 获取调用线程(RM 的主线程)的进程标识
  20.     threadID = GetCurrentThreadId.call
  21.     # 获取 Z 次序中最靠前的窗口
  22.     hWnd = GetWindow.call(GetForegroundWindow.call, 0)
  23.     # 枚举所有窗口
  24.     while hWnd != 0
  25.       # 如果创建该窗口的线程标识匹配本线程标识
  26.       if threadID == GetWindowThreadProcessId.call(hWnd, 0)
  27.         # 分配一个 11 个字节的缓冲区
  28.         className = " " * 11
  29.         # 获取该窗口的类名
  30.         GetClassName.call(hWnd, className, 12)
  31.         # 如果匹配 RGSS Player 则跳出循环
  32.         break if className == "RGSS Player"
  33.       end
  34.       # 获取下一个窗口
  35.       hWnd = GetWindow.call(hWnd, 2)
  36.     end
  37.     return hWnd
  38.   end
  39. end
复制代码
窗口的标题名和类名都不能用来表示一个窗口的唯一性,那么窗口的什么才是唯一的呢?主要有三个:窗口的应用程序实例句柄 hInstance、创建窗口的进程标识 processID、创建窗口的线程标识 threadID。这个脚本用的就是线程的标识,其原理是:枚举桌面上的所有窗口,看看创建它的线程是否匹配当前 RM 的主线程,并且其窗口类名是 RGSS Player(因为 RM 的主线程不只创建了 RM 游戏主窗口这一个窗口),如果是,那么当前获取到的窗口句柄就是我们预期的句柄了~
用法:直接在任意处调用 get_hWnd 就能获取到当前 RM 窗口的句柄~


应用实例,美兽更改窗体分辨率真实版
这个脚本就是在 RM 初始化时调用 FindWindow 来获取窗口句柄。你可以显式地去引发其潜在的问题:先打开一个 Game.exe,窗口分辨率被增加到 800 × 600;再打开一个 Game.exe,窗口出现后立刻切换到 800 × 600 的窗口,就会发现该窗口分辨率又被增加到了 960 × 720 了……
注释掉这个脚本中如下的绿色部分,再添加红色部分,就能解决这个问题:

if $myfirst == nil

  $myfirst = 'myGod'
  宽度=800
  高度=600
#  游戏ini名=".\\Game.ini"
#  val = "\0"*256
#  gps = Win32API.new('kernel32', 'GetPrivateProfileString','pppplp', 'l')
#  gps.call("Game", "Title", "", val, 256, 游戏ini名)
#  val.delete!("\0")
#  title = val
#  fw = Win32API.new('user32', 'FindWindow', 'pp', 'i')
#  hWnd = fw.call("RGSS Player", title)


  hWnd = get_hWnd

  swp = Win32API.new('user32', 'SetWindowPos', 'lliiiii', 'i')

  pointwds = [0,0,0,0].pack('llll')
  pointcet = [0, 0].pack('ll')

  wdsrect = Win32API.new('user32.dll', 'GetWindowRect', 'lp', 'l')
  client_screen = Win32API.new("user32", "ClientToScreen", 'ip', 'i')

  wdsrect.call(hWnd,pointwds)
  client_screen.call(hWnd, pointcet)

  wds = pointwds.unpack('llll')
  cet = pointcet.unpack('ll')

  addw =  wds[2] - wds[0] - 640
  addh =  wds[3] - wds[1] - 480

  x = wds[0] - (宽度 - 640) / 2
  y = wds[1] - (高度 - 480) / 2

  swp.call(hWnd, 0, x, y, 宽度 + addw, 高度 + addh, 0x20)

end

Lv2.观梦者 (暗夜天使)

精灵族の天使

梦石
0
星屑
756
在线时间
3017 小时
注册时间
2007-3-16
帖子
33722

开拓者贵宾

发表于 2009-9-1 09:42:39 | 显示全部楼层
本帖最后由 精灵使者 于 2009-9-1 09:56 编辑

太好了……精灵马上就去测试一下恩恩……
def shot(file = "shot", typ = 1)
    # to add the right extension...
    if typ == 0
      typname = ".bmp"
    elsif typ == 1
      typname = ".jpg"
    elsif typ == 2
      typname = ".png"
    end   
    file_index = 0   
    dir = "Save/"   
    # make the filename....
    file_name = dir + file.to_s + typname.to_s   
    # make the screenshot.... Attention dont change anything from here on....
    @screen.call(0,0,640,480,file_name,get_window_handle,typ)
  end
  # find the game window...
  #def handel
    #game_name = "\0" * 256
    #@readini.call('Game','Title','',game_name,255,".\\Game.ini")
    #game_name.delete!("\0")
    #return @findwindow.call('RGSS Player',game_name)
  #end

end
注释绿色部分,添加红色部分就能解决问题!
太好了,以前的截图脚本千年BUG终于解决了……泪奔
强烈推荐!
回复 支持 反对

使用道具 举报

Lv1.梦旅人

彩色的银子

梦石
0
星屑
50
在线时间
190 小时
注册时间
2006-6-13
帖子
1361

贵宾

发表于 2009-9-1 10:08:45 | 显示全部楼层
呵呵。以前倒真没留意这个问题。对了问个题外话。楼主是以前的那个六脉么?
-.-
回复 支持 反对

使用道具 举报

Lv1.梦旅人

梦石
0
星屑
50
在线时间
0 小时
注册时间
2009-7-7
帖子
246
发表于 2009-9-1 10:45:55 | 显示全部楼层
23.    hWnd = GetWindow.call(GetForegroundWindow.call, 0) 24.    # 枚举所有窗口
初始的参数使用GetDesktopWindow和5更好

threadID = GetCurrentThreadId.call

应该把GetCurrentThreadId改为GetCurrentProcessId…

咳咳…高烧中,如果我看错了,真不好意思

另外那个改窗口大小的,如果游戏内部没有自动适应成符合的分辨率,似乎也没什么意思…咳咳
郑重声明:
任何矛盾与本人绝无半点关系,本人的贴意只是本着看贴不能不回贴(虽然不理解内容)的中华美德,并且本人是文盲伴失忆,越时均不认识和负责,亦不清楚当时所想和所表,而本人所有的贴字均由网络复制而来,并不代表本人有过独立发言或同意或支持或反对或默认某人某事物的观点以及立场,本人只是复习着复制与粘贴的功能
如贴言违反国家有关法律或论坛协约或人情逻辑,请管理人员或不明物体及时删除,而因删贴不及时所引发的法律(包括宪法,刑法,书法,阿尔法,劳动法,婚姻法,输入法,妇女法,没办法,今日说法等可能涉及和未涉及之法律条规)或纠纷或责任,本人概不负责
本人友情谢绝史前动物围观或蛋疼人士的冷嘲热讽或人肉搜索或跨省追捕或卫星偷窥或人身攻击,任何需要都不要联系本人,谢谢
回复 支持 反对

使用道具 举报

Lv2.观梦者 (暗夜天使)

精灵族の天使

梦石
0
星屑
756
在线时间
3017 小时
注册时间
2007-3-16
帖子
33722

开拓者贵宾

发表于 2009-9-1 11:16:39 | 显示全部楼层
本帖最后由 精灵使者 于 2009-9-1 11:48 编辑

我测试一下恩恩……
奶茶的结果……
奶茶的结果还得必须大改……光改哪里还不行恩
回复 支持 反对

使用道具 举报

Lv1.梦旅人

梦石
0
星屑
61
在线时间
24 小时
注册时间
2008-8-5
帖子
1924
 楼主| 发表于 2009-9-1 11:56:42 | 显示全部楼层
呵呵。以前倒真没留意这个问题。对了问个题外话。楼主是以前的那个六脉么?
神思 发表于 2009-9-1 10:11

神思为啥会这么想?
初始的参数使用GetDesktopWindow和5更好

我印象中的 GetForegroundWindow 似乎只获取顶级窗口,若有偏差还望指正 >__<
应该把GetCurrentThreadId改为GetCurrentProcessId…

调用 RM Ruby 解释器和创建窗口的应该是同一个线程,至少目前没发现异常……不过改成判断进程 ID 更保险就是了 ^^
回复 支持 反对

使用道具 举报

Lv2.观梦者 (暗夜天使)

精灵族の天使

梦石
0
星屑
756
在线时间
3017 小时
注册时间
2007-3-16
帖子
33722

开拓者贵宾

发表于 2009-9-1 11:59:18 | 显示全部楼层
使用了防卡脚本以后创建的是两个线程,不是同一个,请注意进程ID更保险一些更好恩。
回复 支持 反对

使用道具 举报

Lv1.梦旅人

梦石
0
星屑
50
在线时间
0 小时
注册时间
2009-7-7
帖子
246
发表于 2009-9-1 12:19:21 | 显示全部楼层
神思为啥会这么想?我印象中的 GetForegroundWindow 似乎只获取顶级窗口,若有偏差还望指正 >__<调用 RM Ruby 解释器和创建窗口的应该是同一个线程,至少目前没发现异常……不过改成判断进程 ID 更保 ...紫苏 发表于 2009-9-1 11:56

你这样获得窗口,任何干涉都会出现意外的…而且你的while既然写成顺序搜索了…那还不如将while写成if(激活的窗口)的ID是指定的进程ID?如果不是return MessageBox(0,"can't find the window","",MB_OK)算了,否则就应该从第一个开始吧
GetWindowThreadProcressId 获取的是进程ID,你用线程的ID和进程的ID比较,也行我就纳闷了…我现在注意到你调用到的没参数了,可能是修改过的…
郑重声明:
任何矛盾与本人绝无半点关系,本人的贴意只是本着看贴不能不回贴(虽然不理解内容)的中华美德,并且本人是文盲伴失忆,越时均不认识和负责,亦不清楚当时所想和所表,而本人所有的贴字均由网络复制而来,并不代表本人有过独立发言或同意或支持或反对或默认某人某事物的观点以及立场,本人只是复习着复制与粘贴的功能
如贴言违反国家有关法律或论坛协约或人情逻辑,请管理人员或不明物体及时删除,而因删贴不及时所引发的法律(包括宪法,刑法,书法,阿尔法,劳动法,婚姻法,输入法,妇女法,没办法,今日说法等可能涉及和未涉及之法律条规)或纠纷或责任,本人概不负责
本人友情谢绝史前动物围观或蛋疼人士的冷嘲热讽或人肉搜索或跨省追捕或卫星偷窥或人身攻击,任何需要都不要联系本人,谢谢
回复 支持 反对

使用道具 举报

Lv1.梦旅人

梦石
0
星屑
61
在线时间
24 小时
注册时间
2008-8-5
帖子
1924
 楼主| 发表于 2009-9-1 12:27:35 | 显示全部楼层
使用了防卡脚本以后创建的是两个线程,不是同一个,请注意进程ID更保险一些更好恩。
精灵使者 发表于 2009-9-1 11:59

呵呵,Ruby 的线程和操作系统的线程是不同的~Ruby 线程是轻量级的、绿色的、跨平台的线程,是由 Ruby 解释器内部在抽象层上,为了程序并行性而实现的可移植机制
而顶楼的脚本用到的是 OS 线程,也就是本地 Windows 线程,通过程序可以列出 RM 进程创建的所有 Windows 线程:
# 1 Priority: 8
# 2 Priority: 8
# 3 Priority: 8
# 4 Priority: 6
# 5 Priority: 15
# 6 Priority: 15
# 7 Priority: 8
# 8 Priority: 15
# 9 Priority: 9
#10 Priority: 15
#11 Priority: 8
#12 Priority: 8
#13 Priority: 5
优先级最高的那几个其中之一应该就是创建窗口且解释 Ruby 脚本的主线程了……
回复 支持 反对

使用道具 举报

Lv2.观梦者 (暗夜天使)

精灵族の天使

梦石
0
星屑
756
在线时间
3017 小时
注册时间
2007-3-16
帖子
33722

开拓者贵宾

发表于 2009-9-1 12:32:05 | 显示全部楼层
恩,原来是这样。
夏娜的10s防卡脚本里面里面有Thread.new来创建新的线程来定期Graphics.update,使主线程来不及刷新的时候帮助其刷新……恩。那个是属于Ruby的线程。看来担心没太大必要呢。
那个截图存档的BUG终于解决了……
这回就没有截到桌面的情况出现了恩。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册会员

本版积分规则

拿上你的纸笔,建造一个属于你的梦想世界,加入吧。
 注册会员
找回密码

站长信箱:[email protected]|手机版|小黑屋|无图版|Project1游戏制作

GMT+8, 2020-5-29 03:43

Powered by Discuz! X3.1

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表