首页 > 代码库 > 在CLR中本地化正在运行的.NET窗口

在CLR中本地化正在运行的.NET窗口

    前段时间在研究某游戏辅助,老外出品,支持七种语言,可这辅助相关的插件却少有中文,因为作者都是老外,并且他们不愿意添加中文。有一些没有加密的插件就被善良的国内用户使用工具软件手工汉化了,但是经过混淆加密的插件就比较困难了,一是需要解密,二是插件数量多更新快,最后弄得只好放弃。有一天,一位用户问我,能不能做一个补丁程序,不去解密也不去修改源程序,只是在窗口显示的时候把文字汉化,他的意思就是hook窗体显示的过程,显然,这是个很不错的想法。

    很多年前,曾经流行过一个名叫“南极星”的软件,后来又出了个“金山快译”,这两个都是可以把程序界面进行本地化的外挂,而我要做的却是补丁,也就是内挂,虽然形式不同,但原理上总是相通的吧。抱着这种想法,开始了新的代码之旅,但没过多久我就彻底放弃了,主要原因是只支持window窗口,对WPF无效,因为WPF窗体中的内容不是有效控件,而且在“自动”方面上更是难以为继。

    我的关子已经卖得够多了吧,你知道的,最终我还是达到了目的。

    .NET的程序只有在在.NET的环境中才会如渔得水般的灵活。对于普通winform和WPF窗体,在窗口构造函数运行之后,我们只需要遍历窗口类的所有私有字段就可以得到全部的控件了,直接修改其对应的文字,这就是汉化的全过程!那么问题来了,一如何拿到窗口的实例引用,二如何在窗口显示以前修改文字,三对于用户自定义的窗体控件如何处理。下面一一解答。

    先来说第二个问题,是通过MethodReplace的不正当手段实现的,原理就是在CLR中修改方法MethodDesc中代码地址来实现的,在调用源方法的方法还没有被Jit前用新方法的地址来代替源方法,使源方法在被调用时整个方法体转向到新方法中(具体过程请Google)。因为显示窗口的方法都是由.NET提供现成编译好的本地代码,是无法替换地址的,所以最好的切入点就是窗口的构造函数。通过当前Domain的AssemblyLoad事件,可以轻易的在程序集被装载时拿到控制权,然后遍历程序集中所有的窗口类,然后对它们的构造函数进行地址转向,这样就抢在了创建窗口的方法被Jit之前。想一想,所有窗口中的控件都需要在构造函数中创建,这通常是通过调用一个名为InitializeComponent的方法,但是这个方法名是不靠谱的,因为它会被混淆,所以仍然要在新方法中使用Opcodes.Calli字段以正确的签名来调用构造方法的源地址(具体请参看MSDN)来完成窗体内控件的初始化过程,这样一来问题就解决了,直接遍历修改即可。等等,怎么修改的内容不全面,还有遗漏。好吧,我忘了有些列表的内容是在窗口第一次被显示时被初始化的,这个问题是VS的IDE造成的,弄得大家都喜欢把用户初始化的数据放在窗口的Load事件中。如何应对?很简单,我把汉化的过程也添加到Load事件中,这样就让汉化的过程发生在窗口原始Load事件之后了。

    在第二个问题完美解决时,第一个问题也被解决了,窗口的构造函数是实例方法,实例方法的第一个参数永远是this,因此,添加事件、反射修改私有字段都得以实现,还可以实现更多本地化以外的事情……

    第三个问题相对就简单多了,唯独的只是繁杂。我们知道绝大多数近件的文字都放在名为Text的属性之中,直接修改或是反射修改就可以,但有少数控仍需要特殊处理,例如:ToolTip控件的Caption属性,菜单的层级结构,列表近件中的内容集合,PropertyGrid中的特性名称。再有,就是一些自定义风格的窗口类,它们控件的文字可能会放在名为Text、Title、SubTitle、Caption、Value1、Value2等等属性中,利用反射遍历查询一下就可以了,当然还要为这些类型和字段做缓存的,这会提升效率。

    后来,功能虽然实现了,但它并不好用,用户体验很差劲,主要是自动翻译的效果非常不理想,但我实在是无法解决这种复杂的专业问题的,所以,果断放弃自动翻译,转为输出列表,然后再有目的的翻译。 

在CLR中本地化正在运行的.NET窗口