├── .gitignore ├── dist ├── 键盘同步器.exe └── config │ ├── mainForm.table │ └── conf.table ├── images └── screenshot.png ├── README.md ├── default.aproj ├── dlg └── addKey.aardio ├── lib └── style.aardio └── main.aardio /.gitignore: -------------------------------------------------------------------------------- 1 | # 过滤文件设置 -------------------------------------------------------------------------------- /dist/键盘同步器.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akiyama-Sanjin/keyclone/HEAD/dist/键盘同步器.exe -------------------------------------------------------------------------------- /dist/config/mainForm.table: -------------------------------------------------------------------------------- 1 | { 2 | edit=""; 3 | afterLoad=null; 4 | beforeSave=null; 5 | combobox=1 6 | } -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akiyama-Sanjin/keyclone/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /dist/config/conf.table: -------------------------------------------------------------------------------- 1 | { 2 | keyList={["1"]=1;["0"]=1;["3"]=1;["2"]=1;["5"]=1;["4"]=1;["7"]=1;["6"]=1;["9"]=1;["8"]=1;Q=1;SPACE=1;E=1;R=1;F=1;TAB=1} 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # keyclone 2 | 一个aardio语言写的键盘同步器,用于wow多开同步。 3 | 4 | # 安装使用 5 | 6 | - 下载/dist/键盘同步器.exe即可,无需其他文件。 7 | 8 | - 选择游戏启动程序,如本地的wow.exe 9 | 10 | - 启动游戏,可以设置多开进程数 11 | 12 | - 点击 **绑定窗口** 会自动检测已启动的游戏窗口句柄,加入监听列表。 13 | 14 | - 点击 **开始同步** 会自动同步,不需要时,点击 **停止同步** 以暂停。 15 | 16 | - 左右两个列表都有右键菜单,对应不同的功能,比如定位和解绑窗口,快捷键管理等。 17 | 18 | # 软件截图 19 | 20 | ![image](https://github.com/theseazhang/keyclone/blob/main/images/screenshot.png) 21 | 22 | # 鸣谢 23 | 24 | 本程序由[aardio](https://www.aardio.com/)开发,正因为aardio封装了key.hook和winex等强大的标准库,才能用很少的代码实现这些功能。 25 | -------------------------------------------------------------------------------- /default.aproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /dlg/addKey.aardio: -------------------------------------------------------------------------------- 1 | import win.ui 2 | import style 3 | /*DSG{{*/ 4 | var winform = win.form(text="新增按键";right=434;bottom=59;border="dialog frame";max=false;min=false) 5 | winform.add( 6 | edit={cls="edit";left=5;top=10;right=300;bottom=50;autohscroll=false;autovscroll=false;edge=1;font=LOGFONT(h=-21);readonly=1;z=1}; 7 | plus={cls="plus";text="确定";left=310;top=10;right=430;bottom=50;bgcolor=32768;font=LOGFONT(h=-21);notify=1;z=2} 8 | ) 9 | /*}}*/ 10 | 11 | import key.hook 12 | var hook = key.hook() 13 | 14 | //录制回调函数 15 | hook.proc = function(msg,vkcode,scancode,injected,flags,timeStamp,extraInfo){ 16 | if(injected) return; //模拟鼠标不处理 17 | var kn = key.getName(vkcode); 18 | select(msg) { 19 | case 0x100/*_WM_KEYDOWN*/ ,0x104/*_WM_SYSKEYDOWN*/ { 20 | winform.edit.text = kn 21 | } 22 | } 23 | } 24 | 25 | winform.plus.skin(style.btn) 26 | winform.plus.oncommand = function(id,event){ 27 | if(hook){ 28 | hook.close() 29 | hook = null 30 | } 31 | winform.endModal(winform.edit.text) 32 | } 33 | 34 | winform.onClose = function(hwnd,message,wParam,lParam){ 35 | if(hook){ 36 | hook.close() 37 | hook = null 38 | } 39 | } 40 | 41 | winform.show(); 42 | win.loopMessage(); 43 | return winform; -------------------------------------------------------------------------------- /lib/style.aardio: -------------------------------------------------------------------------------- 1 | namespace style 2 | 3 | btn = { 4 | background={ 5 | default=0xFF099DFD; 6 | hover=0xFF006AD8; 7 | active=0xFF006AD8; 8 | disabled=0xFF333333; 9 | }; 10 | color={ 11 | default=0xFFFFFFFF; 12 | disabled=0xFFFFFFFF; 13 | }; 14 | } 15 | 16 | btnSuccess = { 17 | background={ 18 | default=0xFF198754; 19 | hover=0xFF157347; 20 | active=0xFF157347; 21 | disabled=0xFF333333; 22 | }; 23 | color={ 24 | default=0xFFFFFFFF; 25 | disabled=0xFFFFFFFF; 26 | }; 27 | } 28 | 29 | btnDanger = { 30 | background={ 31 | default=0xFFDC3545; 32 | hover=0xFFBB2C3B; 33 | active=0xFFBB2C3B; 34 | disabled=0xFF333333; 35 | }; 36 | color={ 37 | default=0xFFFFFFFF; 38 | disabled=0xFFFFFFFF; 39 | }; 40 | } 41 | 42 | btnWarning = { 43 | background={ 44 | default=0xFFffc107; 45 | hover=0xFFFFCA2C; 46 | active=0xFFFFCA2C; 47 | disabled=0xFF333333; 48 | }; 49 | color={ 50 | default=0xFF000000; 51 | disabled=0xFFFFFFFF; 52 | }; 53 | } 54 | 55 | btnInfo = { 56 | background={ 57 | default=0xFFE1E1E1; 58 | hover=0xFF099DFD; 59 | active=0xFF099DFD; 60 | disabled=0xFF333333; 61 | }; 62 | color={ 63 | default=0xFF000000; 64 | hover=0xFFFFFFFF; 65 | active=0xFFFFFFFF; 66 | disabled=0xFFFFFFFF; 67 | }; 68 | } -------------------------------------------------------------------------------- /main.aardio: -------------------------------------------------------------------------------- 1 | //RUNAS// 2 | import win.ui 3 | import win.ui.atom 4 | import win.ui.menu 5 | import style 6 | import console 7 | import key.hook 8 | import winex.key 9 | import fsys.dlg 10 | import fsys.table 11 | import win.cur 12 | import winex 13 | import mouse 14 | import fsys.config 15 | import process 16 | /*DSG{{*/ 17 | mainForm = win.form(text="键盘同步器";right=484;bottom=374;border="dialog frame";max=false) 18 | mainForm.add( 19 | combobox={cls="combobox";left=90;top=35;right=145;bottom=60;edge=1;items={"2";"3";"4";"5";"10";"20";"25"};mode="dropdownlist";z=4}; 20 | edit={cls="edit";left=90;top=5;right=480;bottom=30;edge=1;multiline=1;num=1;z=1}; 21 | listview={cls="listview";left=5;top=65;right=270;bottom=370;bgcolor=16777215;edge=1;fullRow=1;gridLines=1;msel=false;vscroll=1;z=7}; 22 | listview2={cls="listview";left=295;top=65;right=480;bottom=370;bgcolor=16777215;edge=1;fullRow=1;gridLines=1;msel=false;vscroll=1;z=10}; 23 | plus={cls="plus";text="选择游戏";left=5;top=5;right=85;bottom=30;bgcolor=32768;notify=1;z=2}; 24 | plus2={cls="plus";text="启动游戏";left=5;top=35;right=85;bottom=60;bgcolor=32768;notify=1;z=3}; 25 | plus3={cls="plus";text="绑定窗口";left=190;top=35;right=270;bottom=60;bgcolor=32768;notify=1;z=6}; 26 | plus4={cls="plus";text="开始同步";left=295;top=35;right=375;bottom=60;bgcolor=32768;notify=1;z=8}; 27 | plus5={cls="plus";text="停止同步";left=400;top=35;right=480;bottom=60;bgcolor=32768;notify=1;z=9}; 28 | static={cls="static";text="开";left=155;top=35;right=180;bottom=60;center=1;transparent=1;z=5} 29 | ) 30 | /*}}*/ 31 | 32 | var atom, hwnd = mainForm.atom("DD437306-782B-4B3D-81DD-F30F722035BC.键盘同步器") 33 | if(!atom){ 34 | win.setForeground(hwnd) 35 | return; 36 | } 37 | 38 | 39 | var conf = fsys.config(io.appData("/keyclone/config/")); 40 | mainForm.bindConfig(conf.mainForm, { 41 | edit = "text"; 42 | checkbox = "checked"; 43 | combobox = "selIndex"; 44 | }); 45 | 46 | if(!mainForm.combobox.selIndex) mainForm.combobox.selIndex = 1 47 | 48 | var hwnds = {} 49 | 50 | var fstb = fsys.table(io.appData("/keyclone/config/conf.table")) 51 | if(!fstb[['keyList']]){ 52 | fstb[['keyList']] = { 53 | ["0"] = 1; 54 | ["1"] = 1; 55 | ["2"] = 1; 56 | ["3"] = 1; 57 | ["4"] = 1; 58 | ["5"] = 1; 59 | ["6"] = 1; 60 | ["7"] = 1; 61 | ["8"] = 1; 62 | ["9"] = 1; 63 | ["Q"] = 1; 64 | ["E"] = 1; 65 | ["R"] = 1; 66 | ["F"] = 1; 67 | ["SPACE"] = 1; 68 | ["TAB"] = 1; 69 | } 70 | } 71 | 72 | mainForm.plus.skin(style.btn) 73 | mainForm.plus.oncommand = function(id,event){ 74 | var path = fsys.dlg.open("exe文件|*.exe|所有文件|*.*") 75 | if(path) mainForm.edit.text = path 76 | } 77 | 78 | mainForm.plus2.skin(style.btnDanger) 79 | mainForm.plus2.oncommand = function(id,event){ 80 | var path = mainForm.edit.text 81 | if(!#path){ 82 | mainForm.msgboxErr("未选择游戏启动路径") 83 | return; 84 | } 85 | if(!io.exist(path)){ 86 | mainForm.msgboxErr("游戏启动路径错误") 87 | return; 88 | } 89 | var n = tonumber(mainForm.combobox.selText) 90 | for(i=1;n;1){ 91 | process.execute(path) 92 | } 93 | } 94 | 95 | mainForm.listview.setColumns({"序号","窗口句柄","进程ID"}) 96 | 97 | mainForm.popmenu = win.ui.popmenu(mainForm);//创建弹出菜单 98 | 99 | mainForm.listview.onnotify = function(id,code,ptr){ 100 | select(code) { 101 | case 0xFFFFFFFB/*_NM_RCLICK*/ { 102 | mainForm.popmenu.popup(x,y,true);//弹出菜单 103 | } 104 | } 105 | } 106 | 107 | mainForm.popmenu.add('定位窗口',function(id){ 108 | var index = mainForm.listview.selIndex 109 | if(index){ 110 | var hwnd = tonumber(mainForm.listview.getItemText(index, 2)) 111 | win.setForeground(hwnd) 112 | } 113 | }); 114 | 115 | mainForm.popmenu.add('解绑窗口',function(id){ 116 | var index = mainForm.listview.selIndex 117 | if(index){ 118 | var hwnd = tonumber(mainForm.listview.getItemText(index, 2)) 119 | table.removeByValue(hwnds, hwnd) 120 | mainForm.listview.delItem(index) 121 | } 122 | }); 123 | 124 | mainForm.popmenu.add('解绑并关闭',function(id){ 125 | var index = mainForm.listview.selIndex 126 | if(index){ 127 | var hwnd = tonumber(mainForm.listview.getItemText(index, 2)) 128 | table.removeByValue(hwnds, hwnd) 129 | mainForm.listview.delItem(index) 130 | winex.close(hwnd) 131 | } 132 | }); 133 | 134 | mainForm.plus3.skin(style.btn) 135 | mainForm.plus3.oncommand = function(id,event){ 136 | mainForm.plus3.disabled = true 137 | var index = 0 138 | mainForm.listview.clear() 139 | hwnds = {} 140 | var s = ".*.exe" 141 | var path = mainForm.edit.text 142 | if(io.exist(path)) s = io.splitpath(path).file 143 | for processEntry in process.each(s) { 144 | var pid = processEntry.th32ProcessID 145 | var hwnd = winex.findMainWnd(,,pid) 146 | if(hwnd) { 147 | index += 1 148 | mainForm.listview.addItem({index, hwnd, pid}) 149 | table.push(hwnds, hwnd) 150 | } 151 | } 152 | mainForm.plus3.disabled = false 153 | } 154 | 155 | 156 | 157 | mainForm.plus4.skin(style.btnWarning) 158 | mainForm.plus4.oncommand = function(id,event){ 159 | if(!#hwnds){ 160 | mainForm.msgboxErr("请先绑定游戏窗口") 161 | return; 162 | } 163 | mainForm.plus4.disabled = true 164 | hook = key.hook() 165 | hook.proc = function(msg,vkcode,scancode,injected,flags,timeStamp,extraInfo){ 166 | if(injected) return; //模拟鼠标不处理 167 | var kn = key.getName(vkcode) 168 | if(!fstb[['keyList']][[kn]]) return; //只响应指定按键 169 | var currentHwnd = win.getForeground() //当前激活窗口句柄 170 | select(msg) { 171 | case 0x100/*_WM_KEYDOWN*/ ,0x104/*_WM_SYSKEYDOWN*/ { 172 | for(i=1;#hwnds;1){ 173 | if(currentHwnd != hwnds[i]) winex.key.down(hwnds[i], kn) 174 | } 175 | } 176 | case 0x101/*_WM_KEYUP*/,0x105/*_WM_SYSKEYUP*/ { 177 | for(i=1;#hwnds;1){ 178 | if(currentHwnd != hwnds[i]) winex.key.up(hwnds[i], kn) 179 | } 180 | } 181 | } 182 | } 183 | } 184 | 185 | mainForm.plus5.skin(style.btnWarning) 186 | mainForm.plus5.oncommand = function(id,event){ 187 | if(hook) { 188 | hook.close() 189 | hook = null 190 | } 191 | mainForm.plus4.disabled = false 192 | } 193 | 194 | mainForm.onDestroy = function(){ 195 | if(hook) { 196 | hook.close() 197 | hook = null 198 | } 199 | } 200 | 201 | mainForm.listview2.setColumns({"同步按键"}) 202 | 203 | var loadData = function(){ 204 | mainForm.listview2.clear() 205 | for k,v in table.eachName(fstb[['keyList']]){ 206 | mainForm.listview2.addItem(k) 207 | } 208 | } 209 | 210 | loadData() 211 | 212 | 213 | mainForm.popmenu2 = win.ui.popmenu(mainForm);//创建弹出菜单 214 | 215 | mainForm.listview2.onnotify = function(id,code,ptr){ 216 | select(code) { 217 | case 0xFFFFFFFB/*_NM_RCLICK*/ { 218 | mainForm.popmenu2.popup(x,y,true);//弹出菜单 219 | } 220 | } 221 | } 222 | 223 | mainForm.popmenu2.add('删除',function(id){ 224 | var index = mainForm.listview2.selIndex 225 | if(index){ 226 | var x = mainForm.listview2.getItemText(index) 227 | mainForm.listview2.delItem(index) 228 | fstb[['keyList']][[x]] = null 229 | fstb.save() 230 | } 231 | }); 232 | 233 | mainForm.popmenu2.add('新增',function(id){ 234 | var x = mainForm.loadForm("\dlg\addKey.aardio").doModal() 235 | if(#x) { 236 | if(fstb[['keyList']][[x]]){ 237 | mainForm.msgboxErr("该按键已经存在") 238 | return; 239 | } 240 | fstb[['keyList']][[x]] = 1 241 | fstb.save() 242 | loadData() 243 | } 244 | }); 245 | 246 | mainForm.popmenu2.add('清空',function(id){ 247 | if(!mainForm.msgboxTest("确定清空所有按键吗?")) return; 248 | fstb[['keyList']] = {} 249 | fstb.save() 250 | mainForm.listview2.clear() 251 | }); 252 | 253 | mainForm.popmenu2.add('仅数字按键',function(id){ 254 | fstb[['keyList']] = { 255 | ["0"] = 1; 256 | ["1"] = 1; 257 | ["2"] = 1; 258 | ["3"] = 1; 259 | ["4"] = 1; 260 | ["5"] = 1; 261 | ["6"] = 1; 262 | ["7"] = 1; 263 | ["8"] = 1; 264 | ["9"] = 1; 265 | } 266 | fstb.save() 267 | loadData() 268 | }); 269 | 270 | mainForm.popmenu2.add('恢复初始值',function(id){ 271 | fstb[['keyList']] = { 272 | ["0"] = 1; 273 | ["1"] = 1; 274 | ["2"] = 1; 275 | ["3"] = 1; 276 | ["4"] = 1; 277 | ["5"] = 1; 278 | ["6"] = 1; 279 | ["7"] = 1; 280 | ["8"] = 1; 281 | ["9"] = 1; 282 | ["Q"] = 1; 283 | ["E"] = 1; 284 | ["R"] = 1; 285 | ["F"] = 1; 286 | ["SPACE"] = 1; 287 | ["TAB"] = 1; 288 | } 289 | fstb.save() 290 | loadData() 291 | }); 292 | 293 | mainForm.show(); 294 | return win.loopMessage(); --------------------------------------------------------------------------------