分类

类型:
不限 游戏开发 计算机程序开发 Android开发 网站开发 笔记总结 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
原创:
不限
年份:
不限 2018 2019

文章列表

  • 内存快速搜索遍历

    背景相比很多人都用过内存搜索软件 Cheat Engine 吧,它里面提供了强大进程内存搜索功能,搜索速度很快,搜索结果也很精确。我之前对内存搜索也稍微专研了一下,是因为当时需要写一个小程序,那个小程序的功能就是可以搜索出指定进程指定值的内存地址,这个CE就能做,只不过是要在自己的程序里实现内存的搜索。
    内存的遍历搜索,说难也不难,说容易也不容易。因为你可以做得比较简单,也可以做得比较完美,这主要是在搜索效率上的区别而已。简单的搜索方法就是直接暴力搜索内存,直接从0地址搜索到0x7FFFFFFF地址处,因为低 2GB 进程空间是用户空间。然后,匹配值就可以了。难点的,就是过滤掉一些内存地址,不用搜索。例如,进程加载基址之前的地址空间,就可以不用搜索等等,以此来缩小搜索的范围,提升搜索效率。
    本文就是对内存遍历实现快速搜索,当然,这肯定不会是最快的搜索方式,只是相对的快速。我们也是通过加载基址,以及内存空间地址信息,缩小搜索的范围,提升搜索效率。现在,就把实现过程整理成文档,分享给大家。
    函数介绍VirtualQueryEx 函数
    查询地址空间中内存地址的信息。
    函数声明
    DWORD VirtualQueryEx( HANDLE hProcess, LPCVOID lpAddress, PMEMORY_BASIC_INFORMATION lpBuffer, DWORD dwLength);
    参数

    hProcess:进程句柄。lpAddress:查询内存的地址。lpBuffer:指向MEMORY_BASIC_INFORMATION结构的指针,用于接收内存信息。dwLength:MEMORY_BASIC_INFORMATION结构的大小。
    返回值

    返回值是信息缓冲区中返回的实际字节数。如果函数失败,返回值是 0。为了获得更多的错误信息,调用GetLastError。

    MEMORY_BASIC_INFORMATION 结构体
    结构体声明
    typedef struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress; // 区域基地址 PVOID AllocationBase; // 分配基地址 DWORD AllocationProtect; // 区域被初次保留时赋予的保护属性 SIZE_T RegionSize; // 区域大小(以字节为计量单位) DWORD State; // 状态(MEM_FREE、MEM_RESERVE或 MEM_COMMIT) DWORD Protect; // 保护属性 DWORD Type; // 类型} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
    成员

    BaseAddress:与lpAddress参数的值相同,但是四舍五入为页面的边界值。AllocationBas:指明用VirtualAlloc函数分配内存区域的基地址。lpAddress在该区域之内。AllocationProtect:指明该地址空间区域被初次保留时赋予该区域的保护属性。PAGE_READONLY:只读属性,如果试图进行写操作,将引发访问违规。如果系统区分只读、执行两种属性,那么试图在该区域执行代码也将引发访问违规。PAGE_READWRITE:允许读写。PAGE_EXECUTE:只允许执行代码,对该区域试图进行读写操作将引发访问违规。PAGE_EXECUTE_READ:允许执行和读取。PAGE_EXECUTE_READWRITE:允许读写和执行代码。PAGE_EXECUTE_WRITECOPY:对于该地址空间的区域,不管执行什么操作,都不会引发访问违规。如果试图在该页面上的内存中进行写入操作,就会将它自己的私有页面(受页文件的支持)拷贝赋予该进程。PAGE_GUARD:在页面上写入一个字节时使应用程序收到一个通知(通过一个异常条件)。PAGE_NOACCESS:禁止一切访问。PAGE_NOCACHE:停用已提交页面的高速缓存。一般情况下最好不要使用该标志,因为它主要是供需要处理内存缓冲区的硬件设备驱动程序的开发人员使用的。RegionSize 用于指明内存块从基地址即BaseAddress开始的所有页面的大小(以字节为计量单位)这些页面与含有用LpAddress参数设定的地址的页面拥有相同的保护属性、状态和类型。State:用于指明所有相邻页面的状态。MEM_COMMIT:指明已分配物理内存或者系统页文件。MEM_FREE:空闲状态。该区域的虚拟地址不受任何内存的支持。该地址空间没有被保留。该状态下AllocationBase、AllocationProtect、Protect和Type等成员均未定义。MEM_RESERVE:指明页面被保留,但是没有分配任何物理内存。该状态下Protect成员未定。Protect:用于指明所有相邻页面(内存块)的保护属性。这些页面与含有拥有相同的保属性、状态和类型。意义同AllocationProtect。Type:用于指明支持所有相邻页面的物理存储器的类型(MEM_IMAGE,MEM_MAPPED或MEM_PRIVATE)。这些相邻页面拥有相同的保护属性、状态和类型。如果是Windows 98,那么这个成员将总是MEM_PRIVATE 。MEM_IMAGE:指明该区域的虚拟地址原先受内存映射的映像文件(如.exe或DLL文件)的支持,但也许不再受映像文件的支持。例如,当写入模块映像中的全局变量时,“写入时拷贝”的机制将由页文件来支持特定的页面,而不是受原始映像文件的支持。MEM_MAPPED:该区域的虚拟地址原先是受内存映射的数据文件的支持,但也许不再受数据文件的支持。例如,数据文件可以使用“写入时拷贝”的保护属性来映射。对文件的任何写入操作都将导致页文件而不是原始数据支持特定的页面。MEM_PRIVATE:指明该内存区域是私有的。不被其他进程共享。

    ReadProcessMemory 函数
    在指定的进程中从内存区域读取数据。 要读取的整个区域必须可访问或操作失败。
    函数声明
    BOOL WINAPI ReadProcessMemory( _In_ HANDLE hProcess, _In_ LPCVOID lpBaseAddress, _Out_ LPVOID lpBuffer, _In_ SIZE_T nSize, _Out_ SIZE_T *lpNumberOfBytesRead);
    参数

    hProcess [in]具有正在读取的内存的进程的句柄。 句柄必须具有进程的PROCESS_VM_READ访问权限。lpBaseAddress [in]指向从中读取的指定进程中的基地址的指针。 在发生任何数据传输之前,系统将验证指定大小的基地址和内存中的所有数据都可访问以进行读取访问,如果不可访问,则该函数将失败。lpBuffer [out]指向从指定进程的地址空间接收内容的缓冲区的指针。nSize [in]要从指定进程读取的字节数。lpNumberOfBytesRead [out]指向一个变量的指针,该变量接收传输到指定缓冲区的字节数。 如果lpNumberOfBytesRead为NULL,则该参数将被忽略。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为0(零)。 要获取扩展错误信息,请调用GetLastError。

    实现原理实现内存的快速搜索,我们主要是缩小搜索的范围,来提升的搜索效率。缩小搜索范围的方法有,一是过滤掉进程加载基址之前的内存地址;二是获取内存空间地址信息,把内存状态不是 MEM_COMMIT 以及保护属性没有读权限的内存区域都过滤掉。
    所以,获取进程加载基址以及获取内存空间地址信息比较关键。
    大概分为以下 7 个步骤:

    首先,我们调用 OpenProcess 函数根据进程 PID 打开进程,并获取进程的句柄,进程句柄的权限为 PROCESS_ALL_ACCESS
    然后,我们根据进程句柄,获取指定进程的加载基址。对于进程加载基址的获取,我们使用的是 EnumProcessModules 函数来获取。其它的的进程基址获取方法,可以参考本站上其他网友写的“获取指定进程的加载基址”这篇文章
    接着,以进程加载基址作为内存搜索的起始地址,调用 VirtualQueryEx 函数查询地址空间中内存地址的信息,然后将内存页面状态不是MEM_COMMIT 过滤掉,即过滤掉没有分配物理内存或者系统页文件。同时,也把没有读权限的页面属性保护都过滤掉
    通过内存地址的信息过滤之后,我们就可以调用 ReadProcessMemory 函数把对应的内存区域读取到自己进程的缓冲区中
    接着,我们就可以匹配内存,搜索指定的内存,并获取制定进程内存地址
    然后,获取下一块内存区域的起始地址,继续重复上面3、4、5步操作,直到满足退出条件
    最后,我们就释放内存,并关闭进程句柄

    这样,就是先了内存的快速遍历搜索。
    编码实现// 搜索内存BOOL SearchMemory(DWORD dwProcessId, PVOID pSearchBuffer, DWORD dwSearchBufferSize){ // 根据PID, 打开进程获取进程句柄 HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError("OpenProcess"); return FALSE; } // 获取进程加载基址 HMODULE hModule = NULL; ::EnumProcessModules(hProcess, &hModule, sizeof(HMODULE), NULL); // 把加载基址作为遍历内存的起始地址, 开始遍历 BYTE *pSearchAddress = (BYTE *)hModule; MEMORY_BASIC_INFORMATION mbi = {0}; DWORD dwRet = 0; BOOL bRet = FALSE; BYTE *pTemp = NULL; DWORD i = 0; BYTE *pBuf = NULL; while (TRUE) { // 查询地址空间中内存地址的信息 ::RtlZeroMemory(&mbi, sizeof(mbi)); dwRet = ::VirtualQueryEx(hProcess, pSearchAddress, &mbi, sizeof(mbi)); if (0 == dwRet) { break; } // 过滤内存空间, 根据内存的状态和保护属性进行过滤 if ((MEM_COMMIT == mbi.State) && (PAGE_READONLY == mbi.Protect || PAGE_READWRITE == mbi.Protect || PAGE_EXECUTE_READ == mbi.Protect || PAGE_EXECUTE_READWRITE == mbi.Protect)) { // 申请动态内存 pBuf = new BYTE[mbi.RegionSize]; ::RtlZeroMemory(pBuf, mbi.RegionSize); // 读取整块内存 bRet = ::ReadProcessMemory(hProcess, mbi.BaseAddress, pBuf, mbi.RegionSize, &dwRet); if (FALSE == bRet) { ShowError("ReadProcessMemory"); break; } // 匹配内存 for (i = 0; i < (mbi.RegionSize - dwSearchBufferSize); i++) { pTemp = (BYTE *)pBuf + i; if (RtlEqualMemory(pTemp, pSearchBuffer, dwSearchBufferSize)) { // 显示搜索到的地址 printf("0x%p\n", ((BYTE *)mbi.BaseAddress + i)); } } // 释放内存 delete[]pBuf; pBuf = NULL; } // 继续对下一块内存区域进行遍历 pSearchAddress = pSearchAddress + mbi.RegionSize; } // 释放内存, 关闭句柄 if (pBuf) { delete[]pBuf; pBuf = NULL; } ::CloseHandle(hProcess); return TRUE;}
    程序测试我们直接运行程序,对 520.exe 进程进行搜索,搜索值为 0x00905A4D 的地址都哪些,程序成功列举所有的地址。

    总结这个程序,通过以进程加载基址为搜索起始地址、判断地址内存空间信息过滤一些内存状态不是 MEM_COMMIT 以及保护属性没有读权限的内存区域,以此来缩小搜索的范围,提升搜索的效率。
    大家注意理解 VirtualQueryEx 函数以及 ReadProcessMemory 函数的参数使用方式,同时也要注意不同进程内存空间的转换。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2018-11-23 18:38:50
  • 【Cocos Creator 实战教程(2)】——天天酷跑(动画、动作相关)

    一、涉及知识点
    添加背景动画添加人物动作碰撞检测
    二、步骤2.1 准备工作新建一个项目,这回在资源管理器新建一个animation文件夹,用来存放节点动画。
    同时,将背景图拉上,这样背景图动起来时,就相当于人物相对位置再移动,跑了起来。

    2.2 背景动画——让背景动起来为背景节点添加滚动动画 。
    首先在animation新建一个animation clip取名为bg_roll。
    其次为背景节点添加animation组件(在其他组件中),并将bg_roll添加进去。

    然后选择动画编辑器编辑动画。


    这样我们就得到了一个关键帧。
    这里将一下关键帧的作用。
    动画是由一帧一帧的图片构成的,而我们指定了属性的关键帧后,就知道如何控制动画序列的中间步骤。
    例如背景滚动,只需要指定初始位置和结束位置作为关键帧,而人物动作则有多个关键帧。

    最后选上play on load默认自动播放,这样背景就滚动起来了。(记得时间间隔久一点,可以滚动的慢一点)
    2.4 大圣动画这里我们要使用的的是动态加载图片,属性选择cc.spriteframe

    然后我们把要加载的图片拖进来,这里需要注意的是因为是一系列的图片,所以我们最后是生成了一张plist图集,制作方法https://docs.cocos.com/creator/manual/zh/asset-workflow/atlas.html
    接着我们创建关键帧并把大圣跑动的一张一张图片拖入到动画区,最后调整wrapMode为loop让这个动画循环播放。

    2.3 导弹动画这里我们要添加两个Clip,一个是高空导弹,一个是低空导弹 。这时属性就要选择position因为位置(x,y)不同。这节课我们一共用到了三种动画属性,其实还有很多种属性,大家可以查询官网自己学习。



    2.4 为碰撞检测添加事件这里要先讲一下概念。
    虽然我们选择背景为静止坐标系时,大圣和导弹都在相对移动,但是其实在制作过程中,我们的大圣并没有移动,而是背景和导弹在移动。
    所以我们想要选择导弹在大圣附近那几帧图片时,只需把大圣作为静止坐标系让导弹移动。
    具体做法便是选中Bomb然后播放动画等到导弹在大圣附近时停止,插入事件,添加碰撞检测。

    2.5 编写脚本添加动作Game.js
    cc.Class({ extends: cc.Component, properties: { king:{ default:null, type:cc.Node, } }, onLoad: function () { var self = this; //左侧蹲,右侧跳 this.node.on('touchstart',function(event){ var visibleSize = cc.director.getVisibleSize(); if(event.getLocationX()<visibleSize.width/2){ self.king.getComponent('King').down(); }else{ self.king.getComponent('King').jump(); } }); //左侧松手就恢复跑的状态 this.node.on('touchend',function(event){ var visibleSize = cc.director.getVisibleSize(); if(event.getLocationX()<visibleSize.width/2){ self.king.getComponent('King').downRelease(); }else{ // self.king.getComponent('King').jump(); } }); },});
    King.js
    cc.Class({ extends: cc.Component, properties: { // 主角跳跃高度 jumpHeight: 0, // 主角跳跃持续时间 jumpDuration: 0, //主角状态 state:'run', }, //跑 run:function(){ this.getComponent(cc.Animation).play('king_run'); this.state = 'run'; }, //跳 jump:function(){ if(this.state == 'run'){ this.state = 'jump'; this.getComponent(cc.Animation).stop(); this.node.runAction(cc.sequence(cc.jumpBy(this.jumpDuration, cc.p(0,0), this.jumpHeight, 1), cc.callFunc(function() { this.run(); }, this))); } }, //弯腰跑 down:function(){ if(this.state == 'run'){ this.state = 'down'; this.node.runAction(cc.scaleTo(0.05, 1, 0.5)); } }, //腰累了 downRelease:function(){ if(this.state == 'down'){ this.node.runAction(cc.sequence(cc.scaleTo(0.05, 1, 1), cc.callFunc(function() { this.run(); }, this))); } },});
    2.6 完善碰撞检测脚本Bomb.js
    cc.Class({ extends: cc.Component, properties: { king:{ default:null, type:cc.Node, } }, //判断高空导弹来时,猴哥是否蹲下(响应之前设置的帧事件) judgeDown:function(){ if(this.king.getComponent('King').state == 'down'){ console.log("down---------------------"); }else{ cc.director.loadScene('Over'); } }, //判断低空导弹来时,猴哥是否跳起 judgeJump:function(){ if(this.king.getComponent('King').state == 'jump'){ console.log("jump---------------------"); }else{ cc.director.loadScene('Over'); } }, onLoad: function () { let self = this; //每隔2秒随机发射高空和低空导弹 this.schedule(function(){ if(Math.random()>0.5){ this.getComponent(cc.Animation).play('bomb_high'); }else{ this.getComponent(cc.Animation).play('bomb_low'); } },3); },});
    2.7 游戏结束
    Over.js
    cc.Class({ extends: cc.Component, properties: { }, reTry: function(){ cc.director.loadScene('Game'); }, onLoad: function () { },});
    3. 注意
    部分资料素材来源于网络。要自学事件响应的代码,最好有js基础,多参考文档。跳跃和下蹲在现实中会另做一套动画,这里直接使用了改变位置和压缩图片的方法。想想如何替换成另一套动画吧。(提示:动画该开始并不加载(play on road),触屏事件响应时再加载,欢迎在评论区附上心得)这次的碰撞检测做的很简单,实际上还有蒙版法,像素点法等一系列方法,之后会给大家详细介绍。初次写博客请大家多砸评论。
    3 回答 2018-11-22 14:10:27
  • 【Cocos Creator 实战教程(1)】——人机对战五子棋(节点事件相关)

    一、涉及知识点
    场景切换按钮事件监听节点事件监听节点数组循环中闭包的应用动态更换sprite图片定时器预制资源
    二、步骤2.1 准备工作首先,我们要新建一个空白工程,并在资源管理器中新建几个文件夹

    在这些文件夹中,我们用来存放不同的资源,其中

    Scene用来存放场景,我们可以把场景看作一个关卡,当关卡切换时,场景就切换了
    Script用来存放脚本文件
    Texture用来存放显示的资源,例如音频,图片
    Prefab用来存放预制资源,接下来我会详细的介绍

    接下来,我们在Scene文件夹中新建一个场景(右击文件夹->新建->Scene),命名为Menu,接着导入背景图片(直接拖拽即可)。最后调整图片大小使图片铺满背景,效果如图。

    2.2 按钮监听与场景切换接下来我们来学习此次实战的第一个难点,按钮监听与场景切换。
    首先,创建一个Button节点,并删除label,放在人机博弈的按钮上,并在属性上调成透明样式。

    接下来,新建一个Game场景,并添加一个棋盘节点,并把锚点设为(0,0)。

    这里,讲一下锚点的作用。
    anchor point 究竟是怎么回事? 之所以造成不容易理解的是因为我们平时看待一个图片是以图片的中心点这一个维度来决定图片的位置的。而在cocos2d中决定一个图片的位置是由两个维度一个是 position 另外一个是anchor point。只要我们搞清楚他们的关系,自然就迎刃而解。默认情况下,anchor point在图片的中心位置(0.5, 0.5),取值在0到1之间的好处就是,锚点不会和具体物体的大小耦合,也即不用关注物件大小,而应取其对应比率,如果把锚点改成(0,0),则进行放置位置时,以图片左下角作为起始点。也就是说,把position设置成(x,y)时,画到屏幕上需要知道:到底图片上的哪个点放在屏幕的(x,y)上,而anchor point就是这个放置的点,anchor point是可以超过图片边界的,比如下例中的(-1,-1),表示从超出图片左下角一个宽和一个高的地方放置到屏幕的(0,0)位置(向右上偏移10个点才开始到图片的左下角,可以认为向右上偏移了10个点的空白区域)
    他们的关系是这样的(假设actualPosition.x,actualPosition.y是真实图片位置的中点):actualPosition.x = position.x + width*(0.5 - anchor_point.x);acturalPosition.y = position.y + height*(0.5 - anchor_point.y)actualPosition 是sprite实际上在屏幕显示的位置, poistion是 程序设置的, achor_point也是程序设置的。
    然后,我们需要新建一个脚本,Menu.js,并添加开始游戏方法。
    cc.Class({ extends: cc.Component, startGame:function(){ cc.director.loadScene('Game');//这里便是运用导演类进行场景切换的代码 }});
    这里提示以下,编辑脚本是需要下载插件的,我选择了VScode,还是很好用的。
    最后我们将其添加为Menu场景的Canvas的组件(添加组件->脚本组件->menu),并在BtnP2C节点上添加按钮监听响应。


    这样,按钮监听就完成了。现在我们在Menu场景里点击一下人机按钮就会跳转到游戏场景了。
    2.3 预制资源预制资源一般是在场景里面创建独立的子界面或子窗口,即预制资源是存放在资源中,并不是节点中,例如本节课中的棋子。
    现在我们就来学习一下如何制作预制资源。

    再将black节点改名为Chess拖入下面Prefab文件夹使其成为预制资源。
    这其中,SpriteFrame 是核心渲染组件 Sprite 所使用的资源,设置或替换 Sprite 组件中的 spriteFrame 属性,就可以切换显示的图像,将其去掉防止图片被预加载,即棋子只是一个有大小的节点不显示图片也就没有颜色。
    2.4 结束场景直接在Game场景上制作结束场景。

    2.5 游戏脚本制作人机对战算法参考了这里https://blog.csdn.net/onezeros/article/details/5542379
    具体步骤便是初始化棋盘上225个棋子节点,并为每个节点添加事件,点击后动态显示棋子图片。
    代码如下:
    cc.Class({ extends: cc.Component, properties: { overSprite:{ default:null, type:cc.Sprite, }, overLabel:{ default:null, type:cc.Label }, chessPrefab:{//棋子的预制资源 default:null, type:cc.Prefab }, chessList:{//棋子节点的集合,用一维数组表示二维位置 default: [], type: [cc.node] }, whiteSpriteFrame:{//白棋的图片 default:null, type:cc.SpriteFrame }, blackSpriteFrame:{//黑棋的图片 default:null, type:cc.SpriteFrame }, touchChess:{//每一回合落下的棋子 default:null, type:cc.Node, visible:false//属性窗口不显示 }, gameState:'white', fiveGroup:[],//五元组 fiveGroupScore:[]//五元组分数 }, //重新开始 startGame(){ cc.director.loadScene("Game"); }, //返回菜单 toMenu(){ cc.director.loadScene("Menu"); }, onLoad: function () { this.overSprite.node.x = 10000;//让结束画面位于屏幕外 var self = this; //初始化棋盘上225个棋子节点,并为每个节点添加事件 for(var y = 0;y<15;y++){ for(var x = 0;x < 15;x++){ var newNode = cc.instantiate(this.chessPrefab);//复制Chess预制资源 this.node.addChild(newNode); newNode.setPosition(cc.p(x*40+20,y*40+20));//根据棋盘和棋子大小计算使每个棋子节点位于指定位置 newNode.tag = y*15+x;//根据每个节点的tag就可以算出其二维坐标 newNode.on(cc.Node.EventType.TOUCH_END,function(event){ self.touchChess = this; if(self.gameState === 'black' && this.getComponent(cc.Sprite).spriteFrame === null){ this.getComponent(cc.Sprite).spriteFrame = self.blackSpriteFrame;//下子后添加棋子图片使棋子显示 self.judgeOver(); if(self.gameState == 'white'){ self.scheduleOnce(function(){self.ai()},1);//延迟一秒电脑下棋 } } }); this.chessList.push(newNode); } } //开局白棋(电脑)在棋盘中央下一子 this.chessList[112].getComponent(cc.Sprite).spriteFrame = this.whiteSpriteFrame; this.gameState = 'black'; //添加五元数组 //横向 for(var y=0;y<15;y++){ for(var x=0;x<11;x++){ this.fiveGroup.push([y*15+x,y*15+x+1,y*15+x+2,y*15+x+3,y*15+x+4]); } } //纵向 for(var x=0;x<15;x++){ for(var y=0;y<11;y++){ this.fiveGroup.push([y*15+x,(y+1)*15+x,(y+2)*15+x,(y+3)*15+x,(y+4)*15+x]); } } //右上斜向 for(var b=-10;b<=10;b++){ for(var x=0;x<11;x++){ if(b+x<0||b+x>10){ continue; }else{ this.fiveGroup.push([(b+x)*15+x,(b+x+1)*15+x+1,(b+x+2)*15+x+2,(b+x+3)*15+x+3,(b+x+4)*15+x+4]); } } } //右下斜向 for(var b=4;b<=24;b++){ for(var y=0;y<11;y++){ if(b-y<4||b-y>14){ continue; }else{ this.fiveGroup.push([y*15+b-y,(y+1)*15+b-y-1,(y+2)*15+b-y-2,(y+3)*15+b-y-3,(y+4)*15+b-y-4]); } } } }, //电脑下棋逻辑 ai:function(){ //评分 for(var i=0;i<this.fiveGroup.length;i++){ var b=0;//五元组里黑棋的个数 var w=0;//五元组里白棋的个数 for(var j=0;j<5;j++){ this.getComponent(cc.Sprite).spriteFrame if(this.chessList[this.fiveGroup[i][j]].getComponent(cc.Sprite).spriteFrame == this.blackSpriteFrame){ b++; }else if(this.chessList[this.fiveGroup[i][j]].getComponent(cc.Sprite).spriteFrame == this.whiteSpriteFrame){ w++; } } if(b+w==0){ this.fiveGroupScore[i] = 7; }else if(b>0&&w>0){ this.fiveGroupScore[i] = 0; }else if(b==0&&w==1){ this.fiveGroupScore[i] = 35; }else if(b==0&&w==2){ this.fiveGroupScore[i] = 800; }else if(b==0&&w==3){ this.fiveGroupScore[i] = 15000; }else if(b==0&&w==4){ this.fiveGroupScore[i] = 800000; }else if(w==0&&b==1){ this.fiveGroupScore[i] = 15; }else if(w==0&&b==2){ this.fiveGroupScore[i] = 400; }else if(w==0&&b==3){ this.fiveGroupScore[i] = 1800; }else if(w==0&&b==4){ this.fiveGroupScore[i] = 100000; } } //找最高分的五元组 var hScore=0; var mPosition=0; for(var i=0;i<this.fiveGroupScore.length;i++){ if(this.fiveGroupScore[i]>hScore){ hScore = this.fiveGroupScore[i]; mPosition = (function(x){//js闭包 return x; })(i); } } //在最高分的五元组里找到最优下子位置 var flag1 = false;//无子 var flag2 = false;//有子 var nPosition = 0; for(var i=0;i<5;i++){ if(!flag1&&this.chessList[this.fiveGroup[mPosition][i]].getComponent(cc.Sprite).spriteFrame == null){ nPosition = (function(x){return x})(i); } if(!flag2&&this.chessList[this.fiveGroup[mPosition][i]].getComponent(cc.Sprite).spriteFrame != null){ flag1 = true; flag2 = true; } if(flag2&&this.chessList[this.fiveGroup[mPosition][i]].getComponent(cc.Sprite).spriteFrame == null){ nPosition = (function(x){return x})(i); break; } } //在最最优位置下子 this.chessList[this.fiveGroup[mPosition][nPosition]].getComponent(cc.Sprite).spriteFrame = this.whiteSpriteFrame; this.touchChess = this.chessList[this.fiveGroup[mPosition][nPosition]]; this.judgeOver(); }, judgeOver:function(){ var x0 = this.touchChess.tag % 15; var y0 = parseInt(this.touchChess.tag / 15); //判断横向 var fiveCount = 0; for(var x = 0;x < 15;x++){ if((this.chessList[y0*15+x].getComponent(cc.Sprite)).spriteFrame === this.touchChess.getComponent(cc.Sprite).spriteFrame){ fiveCount++; if(fiveCount==5){ if(this.gameState === 'black'){ this.overLabel.string = "你赢了"; this.overSprite.node.x = 0; }else{ this.overLabel.string = "你输了"; this.overSprite.node.x = 0; } this.gameState = 'over'; return; } }else{ fiveCount=0; } } //判断纵向 fiveCount = 0; for(var y = 0;y < 15;y++){ if((this.chessList[y*15+x0].getComponent(cc.Sprite)).spriteFrame === this.touchChess.getComponent(cc.Sprite).spriteFrame){ fiveCount++; if(fiveCount==5){ if(this.gameState === 'black'){ this.overLabel.string = "你赢了"; this.overSprite.node.x = 0; }else{ this.overLabel.string = "你输了"; this.overSprite.node.x = 0; } this.gameState = 'over'; return; } }else{ fiveCount=0; } } //判断右上斜向 var f = y0 - x0; fiveCount = 0; for(var x = 0;x < 15;x++){ if(f+x < 0 || f+x > 14){ continue; } if((this.chessList[(f+x)*15+x].getComponent(cc.Sprite)).spriteFrame === this.touchChess.getComponent(cc.Sprite).spriteFrame){ fiveCount++; if(fiveCount==5){ if(this.gameState === 'black'){ this.overLabel.string = "你赢了"; this.overSprite.node.x = 0; }else{ this.overLabel.string = "你输了"; this.overSprite.node.x = 0; } this.gameState = 'over'; return; } }else{ fiveCount=0; } } //判断右下斜向 f = y0 + x0; fiveCount = 0; for(var x = 0;x < 15;x++){ if(f-x < 0 || f-x > 14){ continue; } if((this.chessList[(f-x)*15+x].getComponent(cc.Sprite)).spriteFrame === this.touchChess.getComponent(cc.Sprite).spriteFrame){ fiveCount++; if(fiveCount==5){ if(this.gameState === 'black'){ this.overLabel.string = "你赢了"; this.overSprite.node.x = 0; }else{ this.overLabel.string = "你输了"; this.overSprite.node.x = 0; } this.gameState = 'over'; return; } }else{ fiveCount=0; } } //没有输赢交换下子顺序 if(this.gameState === 'black'){ this.gameState = 'white'; }else{ this.gameState = 'black'; } }});
    这里便用到了节点监听,节点数组,定时器和动态更换sprite图片。
    新建Game脚本添加到ChessBoard节点下

    3 注意课程到这里就结束了,本课程部分资源来源于网络。
    第一次写技术分享,如果大家有什么好的建议和问题请在下方留言区留言。
    3 回答 2018-11-21 20:32:10
  • 创建计划任务实现开机自启动

    背景想必实现程序开机自启动,是很常见的功能了。无论是恶意程序,还是正常的应用软件,都会提供这个功能,方便用户的使用。程序开机自启动,顾名思义,就是计算机开机后,不用人为地去运行程序,程序就可以自己运行起来。对于这个功能的,一直都是杀软重点监测的地方。因为,对于病毒来说,重要的不是如何被破坏,而是如何启动。
    在过去写的大大小小的程序中,我也实现过程序自启动的功能。现在,我把这些自启动功能的实现方式进行下总结。常见的方式有:修改开机自启动注册表、开机自启动目录、创建开机自启计划任务、创建开机自启系统服务等方式。现在对这些技术一一进行分析,并形成文档分享给大家。本文介绍的是创建自启动任务计划实现开机自启动的方式,其它的实现方式可以搜索我写的相关系列的文档。
    实现原理我是使用Windows Shell编程实现创建任务计划,所以会涉及COM组件相关知识。
    现在,为方便大家的理解,我把整个程序的逻辑概括为 3 各部分,分别是:初始化操作、创建任务计划以及删除任务计划。现在,我们一一对每一个部分进行解析。
    初始化操作初始化操作的目的就是获取 ITaskService 对象以及 ITaskFolder 对象,我们创建任务计划,主要是对这两个指针进行操作。具体流程是:

    首先初始化COM接口环境,因为我们接下来会使用到COM组件
    然后,创建 ITaskService 对象,并连接到任务服务上
    接着,从 ITaskService 对象中获取 ITaskFolder 对象

    这样,初始化操作就完成了,接下来就是直接使用 ITaskService 对象以及 ITaskFolder 对象进行操作了。
    创建任务计划现在,解析下任务计划的具体创建过程:

    首先,从 ITaskService 对象中创建一个任务定义对象 ITaskDefinition,用来来创建任务
    接着,就是开始对任务定义对象 ITaskDefinition进行设置:

    设置注册信息,包括设置作者的信息
    设置主体信息,包括登陆类型、运行权限
    设置设置信息,包括设置在使用电池运行时是否停止、在使用电池是是否允许运行、是否允许手动运行、是否设置多个实例
    设置操作信息,包括启动程序,并设置运行程序的路径和参数
    设置触发器信息,包括用户登录时触发

    最后,使用 ITaskFolder 对象根据任务定义对象 ITaskDefinition的设置,注册任务计划

    这样,任务计划创建的操作就完成了,只要满足设置的触发条件,那么就会启动指定程序。
    删除任务计划 ITaskFolder 对象存储着已经注册成功的任务计划的信息,我们只需要将任务计划的名称传入其中,调用DeleteTask接口函数,就可以删除指定的任务计划了。
    编码实现创建任务计划的初始化CMyTaskSchedule::CMyTaskSchedule(void){ m_lpITS = NULL; m_lpRootFolder = NULL; // 初始化COM HRESULT hr = ::CoInitialize(NULL); if(FAILED(hr)) { ShowError("CoInitialize", hr); } // 创建一个任务服务(Task Service)实例 hr = ::CoCreateInstance(CLSID_TaskScheduler, NULL, CLSCTX_INPROC_SERVER, IID_ITaskService, (LPVOID *)(&m_lpITS)); if(FAILED(hr)) { ShowError("CoCreateInstance", hr); } // 连接到任务服务(Task Service) hr = m_lpITS->Connect(_variant_t(), _variant_t(), _variant_t(), _variant_t()); if(FAILED(hr)) { ShowError("ITaskService::Connect", hr); } // 获取Root Task Folder的指针,这个指针指向的是新注册的任务 hr = m_lpITS->GetFolder(_bstr_t("\\"), &m_lpRootFolder); if(FAILED(hr)) { ShowError("ITaskService::GetFolder", hr); }}
    创建任务计划BOOL CMyTaskSchedule::NewTask(char *lpszTaskName, char *lpszProgramPath, char *lpszParameters, char *lpszAuthor){ if(NULL == m_lpRootFolder) { return FALSE; } // 如果存在相同的计划任务,则删除 Delete(lpszTaskName); // 创建任务定义对象来创建任务 ITaskDefinition *pTaskDefinition = NULL; HRESULT hr = m_lpITS->NewTask(0, &pTaskDefinition); if(FAILED(hr)) { ShowError("ITaskService::NewTask", hr); return FALSE; } /* 设置注册信息 */ IRegistrationInfo *pRegInfo = NULL; CComVariant variantAuthor(NULL); variantAuthor = lpszAuthor; hr = pTaskDefinition->get_RegistrationInfo(&pRegInfo); if(FAILED(hr)) { ShowError("pTaskDefinition::get_RegistrationInfo", hr); return FALSE; } // 设置作者信息 hr = pRegInfo->put_Author(variantAuthor.bstrVal); pRegInfo->Release(); /* 设置登录类型和运行权限 */ IPrincipal *pPrincipal = NULL; hr = pTaskDefinition->get_Principal(&pPrincipal); if(FAILED(hr)) { ShowError("pTaskDefinition::get_Principal", hr); return FALSE; } // 设置登录类型 hr = pPrincipal->put_LogonType(TASK_LOGON_INTERACTIVE_TOKEN); // 设置运行权限 // 最高权限 hr = pPrincipal->put_RunLevel(TASK_RUNLEVEL_HIGHEST); pPrincipal->Release(); /* 设置其他信息 */ ITaskSettings *pSettting = NULL; hr = pTaskDefinition->get_Settings(&pSettting); if(FAILED(hr)) { ShowError("pTaskDefinition::get_Settings", hr); return FALSE; } // 设置其他信息 hr = pSettting->put_StopIfGoingOnBatteries(VARIANT_FALSE); hr = pSettting->put_DisallowStartIfOnBatteries(VARIANT_FALSE); hr = pSettting->put_AllowDemandStart(VARIANT_TRUE); hr = pSettting->put_StartWhenAvailable(VARIANT_FALSE); hr = pSettting->put_MultipleInstances(TASK_INSTANCES_PARALLEL); pSettting->Release(); /* 创建执行动作 */ IActionCollection *pActionCollect = NULL; hr = pTaskDefinition->get_Actions(&pActionCollect); if(FAILED(hr)) { ShowError("pTaskDefinition::get_Actions", hr); return FALSE; } IAction *pAction = NULL; // 创建执行操作 hr = pActionCollect->Create(TASK_ACTION_EXEC, &pAction); pActionCollect->Release(); /* 设置执行程序路径和参数 */ CComVariant variantProgramPath(NULL); CComVariant variantParameters(NULL); IExecAction *pExecAction = NULL; hr = pAction->QueryInterface(IID_IExecAction, (PVOID *)(&pExecAction)); if(FAILED(hr)) { pAction->Release(); ShowError("IAction::QueryInterface", hr); return FALSE; } pAction->Release(); // 设置程序路径和参数 variantProgramPath = lpszProgramPath; variantParameters = lpszParameters; pExecAction->put_Path(variantProgramPath.bstrVal); pExecAction->put_Arguments(variantParameters.bstrVal); pExecAction->Release(); /* 创建触发器,实现用户登陆自启动 */ ITriggerCollection *pTriggers = NULL; hr = pTaskDefinition->get_Triggers(&pTriggers); if (FAILED(hr)) { ShowError("pTaskDefinition::get_Triggers", hr); return FALSE; } // 创建触发器 ITrigger *pTrigger = NULL; hr = pTriggers->Create(TASK_TRIGGER_LOGON, &pTrigger); if (FAILED(hr)) { ShowError("ITriggerCollection::Create", hr); return FALSE; } /* 注册任务计划 */ IRegisteredTask *pRegisteredTask = NULL; CComVariant variantTaskName(NULL); variantTaskName = lpszTaskName; hr = m_lpRootFolder->RegisterTaskDefinition(variantTaskName.bstrVal, pTaskDefinition, TASK_CREATE_OR_UPDATE, _variant_t(), _variant_t(), TASK_LOGON_INTERACTIVE_TOKEN, _variant_t(""), &pRegisteredTask); if(FAILED(hr)) { pTaskDefinition->Release(); ShowError("ITaskFolder::RegisterTaskDefinition", hr); return FALSE; } pTaskDefinition->Release(); pRegisteredTask->Release(); return TRUE;}
    删除任务计划BOOL CMyTaskSchedule::Delete(char *lpszTaskName){ if(NULL == m_lpRootFolder) { return FALSE; } CComVariant variantTaskName(NULL); variantTaskName = lpszTaskName; HRESULT hr = m_lpRootFolder->DeleteTask(variantTaskName.bstrVal, 0); if(FAILED(hr)) { return FALSE; } return TRUE;}
    程序测试在 main 函数中调用上述封装好的函数,进行测试。main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ CMyTaskSchedule task; BOOL bRet = FALSE; // 创建 任务计划 bRet = task.NewTask("520", "C:\\Users\\DemonGan\\Desktop\\520.exe", "", ""); if (FALSE == bRet) { printf("Create Task Schedule Error!\n"); } // 暂停 printf("Create Task Schedule OK!\n"); system("pause"); // 卸载 任务计划 bRet = task.Delete("520"); if (FALSE == bRet) { printf("Delete Task Schedule Error!\n"); } printf("Delete Task Schedule OK!\n"); system("pause"); return 0;}
    测试结果:
    “以管理员身份运行程序”方式打开程序,提示任务计划创建成功。

    打开任务计划列表进行查看,发现“520”任务计划成功创建。



    然后,删除创建的任务计划,程序提示删除成功。

    查看任务计划列表,发现“520”任务计划已经成功删除。

    所以,测试成功。
    总结这个功能的实现涉及到COM组件的相关知识,所以初学者对此可能感到陌生,这是很正常的。其实,对于这方面的理解我也没有什么好的建议,只有多动手练几遍,加深程序逻辑和印象吧。
    注意的地方就是,创建任务计划,要求程序必须要有管机员权限才行。所以,测试的时候,不要忘记以管理员身份运行程序。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-11-20 14:39:47
  • U盘量产之更改U盘容量大小

    背景由于某个项目,使我无意中接触到了U盘量产方面的操作。刚开始听到“U盘量产”的词语,还以为是要生产U盘的意思。后来了解了之后发现,原来意思也是特别相近了。“U盘量产”就是指U盘生产的最后一个步骤,使用工具对U盘的主控芯片刷写数据信息,也就是刷写如:生产厂商、主控芯片型号、U盘容量、U盘类型等等。
    在熟悉了U盘量产的操作之后,恍然发现,这个操作,有利也有弊啊。利是因为我们可以对一些坏的U盘重新量产一下,还能继续使用,可以修复一些U盘问题;弊是因为这个量产技术也可以被用来做坏事,比如:假容量U盘。也就是,坏人可以使用量产工具,将一个比较小容量的U盘,比如4G,量产为32G或是64G的U盘,然后进行出售。但,U盘实际上只能使用4G容量的大小。
    所以,本文就从坏人的角度,为大家演示更改U盘容量大小这方面的操作,给大家提个醒。注意:请不要使用该方面操作做不好的事,对于造成的后果,本人概不负责。这里,只从技术的角度,与大家交流分享。
    准备工作
    芯片精灵:ChipGenius v4.00.1024版本;
    量产工具:慧荣量产工具;
    U盘:U盘主控芯片是慧荣的8GU盘一个。

    量产步骤1. 插入U盘先插入U盘到计算机,可以看到U盘的容量显示为“7.52G”,而且360也显示为“7.5G”大小。


    2. 查看U盘的主控芯片型号然后,运行芯片精灵“ChipGenius”程序,会看到插入的U盘的检测信息。其中,我们只需要关心“Controller Part-Number”部分的信息“SM3257ENLT”。意思是说,该U盘的主控芯片型号为“SM3257ENLT”。

    3. 下载相应主控芯片型号的量产工具这里需要注意的是,并不是所有U盘的主控芯片都会有公开的量产工具。也就是说,如果U盘生产商不公开量产工具的话,也基本上就很难进行量产了。因为不同的芯片型号,量产工具不一定相同,量产工具并不是通用的。
    本文测试采用的是慧荣主控芯片的U盘,而慧荣主控芯片的量产工具是公开的,所以可以轻松获取到。
    下载“SM3257ENLT”型号对应的量产工具到本地上,并运行。
    4. 量产设置运行量产工具,点击右侧的“Scan USB(F5)”,扫描U盘。

    点击“Setting”,对量产进行设置。

    我们可以看到U盘量产的很多信息,例如U盘的类型、格式、生产商的信息等等。我们都可以进行更改,但是,本文的目的是更改U盘的容量大小。

    所以,点击“Capacity Setting”,跳转到容量设置页面,进行容量大小的设置。选中“Fix”固定大小选项,然后输入U盘的大小的信息,如输入:0-15000M,也就是大约15G左右。然后点击“OK”,保存设置。

    回到主界面后,点击“Start(Space Key)”开始按设置进行U盘量产。

    此时,会弹出提示框询问是否要擦除坏块,点击“确定”,即开始进行量产。

    等待一会儿之后,U盘变量产好了。

    这是,我们点击右侧的“Quit”,退出量产工具程序,并拔出U盘,然后再重新插上,查看U盘容量是否变化。


    这时,U盘容量增大了,说明U盘量产成功了。
    总结再次提醒,千万千万不要用来做坏事哦!千万千万不要用来做坏事哦!千万千万不要用来做坏事哦!
    即使上面显示U盘容量增加了,实际上也只有8G的容量,也只能使用8G的容量。如果使用超过8G的容量,U盘写入数据的时候,便会出现问题的。
    3 回答 2018-11-12 11:28:34
  • 使用WinDbg双机调试SYS无源码驱动程序

    背景有很多学习逆向的小伙伴,逆向一些用户层的程序很熟练了,但是由于没有接触过内核驱动开发,所以对于驱动程序的逆向无从下手。
    对于驱动程序的调试可以分为有源码调试和无源码调试。本文主要讨论无源码驱动程序的调试,也就是逆向驱动程序的方法和步骤。本文演示的是使用 VMware 虚拟机和 WinDbg 程序实现双击调试。
    实现过程VMware虚拟机设置
    打开相应 WMware 虚拟机上的 “Edit virtaul machine settings”。


    “Hardware”选项中 —> 点击“Add”,添加一个串口设备 Serial Port 。如果有打印机(Printer)存在,则先移除虚拟机的 打印机 硬件,然后再添加串口设备 Serial Port,因为打印机会占用串口 COM1。


    “Next”,在“Serial Port” 里选中 “Output to named pipe”。


    “next”,然后如下设置:


    “Finish”之后,回到如下“Virtual Machine Settings”页面,在“I/O Mode” 里选中“Yield CPU on poll”。


    点击“OK”之后,WMware 虚拟机设置就完成了。接下来,我们开机,进入虚拟机系统中,并对虚拟机系统进行设置,将其设置成调试模式。
    虚拟机里的操作系统设置
    如果操作系统不是 Win10,则开机进入桌面后,在运行窗口输入 msconfig —> 引导 —> 高级选项 —> 调试 —> 确定。


    如果操作系统是 Win10,则

    在设置 —> 安全和更新 —> 针对开发人员 —> 开发人员模式;
    管理员身份运行CMD,输入 bcdedit /set testsigning on 开启测试模式;
    在运行窗口输入 msconfig —> 引导 —> 高级选项 —> 调试 —> 确定;






    关机重启,这样虚拟机里的操作系统就设置完成了。接下来,就开始在真机系统上对 WinDbg 程序进行设置并下断点调试了。
    使用 WinDbg 程序开始双机调试无源码驱动程序
    我们在真机上以管理员身份运行 WinDbg,点击 File —> Kernel Debug —> COM,然后在 Port中输入:\\.\pipe\com_1,其它都勾选上,点击“确定”。


    通常 WinDbg 就会一直显示 等待建立连接(Waiting to reconnect…) 状态,如下面所示:


    这时,需要我们点击 Break(Ctrl+Break) 暂停调试。这样,虚拟机就会停下来,我们就可以在 WinDbg 中输入命令。



    我们就可以输入命令,使用 WinDbg 程序来调试虚拟机中的操作系统内核。先来介绍些常用的 WinDbg 命令:
    lm :表示列举虚拟机加载和卸载的内核模块起始地址和结束地址。bu、bp :下断点。u、uf :反汇编指定地址处的代码。dd : 查看指定地址处的数据。dt : 查看数据类型定义。
    其中,bu 命令用来设置一个延迟的、以后再求解的断点,对未加载模块中的代码设置断点;当指定模块被加载时,才真正设置这个断点;这对动态加载模块的入口函数或初始化代码处加断点特别有用。
    在模块没有被加载的时候,bp 断点会失败(因为函数地址不存在),而 bu 断点则可以成功。新版的 WinDBG 中 bp 失败后会自动被转成 bu。
    那么,在无源码的驱动程序 .sys 的入口点函数 DriverEntry 中下断点的指令为:
    bp 驱动模块名称+驱动PE结构入口点函数偏移// 如:bp DriverEnum+0x1828

    我们演示调试无源码驱动程序 DriverEnum.sys,虚拟机系统环境是 Win10 64位。然后,在 WinDbg 程序中输入指令:bp DriverEnum+0x1828。其中,bp表示下断点;DriverEnum 表示驱动程序 DriverEnum.sys 的驱动模块名称;0x1828 表示驱动程序 DriverEnum.sys 的入口点偏移地址,这个偏移地址可以由 PE 查看工具查看得出,如下图:

    输入完下断点指令后,我们在输入 g,让系统继续执行,直到执行到断点处,便会停下。

    我们,在虚拟机系统里,加载并启动我们的驱动程序 DriverEnum.sys,在启动之后,真机上的 WinDbg 成功在 DriverEnum.sys 的入口点 DriverEntry 处停下。这时,我们可以使用快捷键 F10 或者 F10 步过或者步入来单步调试,还可以继续使用 bp 下多个断点。

    总结步骤并不复杂,只是啰嗦而已。大家细心点跟着上述教程,认真操作就可以成功对无源码的驱动程序的入口点函数 DriverEntry 下断点,实现调试。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2018-11-11 09:00:44
  • 在DllMain中测试调用LoadLibrary和CreateThread函数可正常使用

    背景某天,在网上无意中看到一篇博文,标题是说在DLL的入口点函数DllMain中不能调用 LoadLibrary 加载DLL,因为会造成死锁。看到这里我楞了一下,因为我之前写过很多DLL程序,在入口点函数DllMain中也加载过其它的DLL,从没有出现过什么问题。然后,我便仔细阅读了这篇博文,大概理解了它的意思。它应该想表达的是在DLL的DllMain函数中谨慎使用 LoadLibrary,以防发生死锁情况。
    虽然都懂了它的意思,我还是决定自己再亲自动手写一下代码看看。现在,我就把实现的过程和测试心得整理成文档,分享给大家。
    实现过程首先,我直接创建一个名为 Dll_LoadLibrary_CreateThread_Test 的DLL项目工程,在 DllMain 的 DLL_PROCESS_ATTACH 时候直接调用 CreateThread 函数创建一个多线程,同时,也调用 LoadLibrary 加载另一个 DLL 文件。
    BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { // 创建多线程 ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, NULL); // 加载DLL HMODULE hDll = ::LoadLibrary("testdll.dll"); if (NULL == hDll) { ::MessageBox(NULL, "load testdll.dll error.", "error", MB_OK); } break; } … …(省略) } return TRUE;}
    其中,ThreadProc 多线程函数,执行的操作就是每个 5000 毫秒就弹窗提示一次。
    UINT ThreadProc(LPVOID lpVoid){ while (TRUE) { Sleep(5000); ::MessageBox(NULL, "this si from createthread.", "createthread", MB_OK); } return 0;}
    在 testdll.dll 这个DLL的入口点函数DllMain中,直接弹窗提示。
    BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { // 弹窗 ::MessageBox(NULL, "this is from testdll.dll DllMain function.", "testdll.dll", MB_OK); break; } … …(省略) } return TRUE;}
    然后,我们直接加载 Dll_LoadLibrary_CreateThread_Test.dll 文件,成功弹出下图所示的两个提示框,说明 CreateThread 函数成功创建多线程,LoadLibrary 成功加载DLL。


    总结经过上面的例子测试,说明在 DllMain 函数中,是可以使用 CreateThread 以及 LoadLibrary 函数的。只要我们避免相互在 DllMain 中相互调用,出现死锁,如下面这种相互调用的情况:

    DllB 在 DllMain 里调用 LoadLibrary 加载 DllA
    DllA 在 DllMain 里调用 LoadLibrary 加载 DllB

    这样,就会无限循环加载下去,形成死锁。
    所以,之所以说 在DllMain里不能调用LoadLibrary函数,其实,并不是说只要是在 DllMain 中,都不能调用 LoadLibrary 函数,而是说,如果这两个 Dll 如果在 DllMain 中相互调用的情况下,是会出错的,所以,为了避免这种相互调用死锁的情况发生,就不提倡在 DllMain 中调用 LoadLibrary!实际上,只要避免这种相互调用的情况,LoadLibrary 还是可以使用的!
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-11-07 11:31:42
  • 为程序创建任务栏右下角托盘图标

    背景我们用过很多应用程序,无论是功能复杂的大程序还是功能单一的小程序,大都会在计算机右下角显示一个图盘图标,然后我们可以直接把窗口关掉,通过点击托盘图标来控制程序的执行操作或显示窗口。使得程序使用起来比较便捷,不用显示程序主窗口就能完成一些操作,增强了程序的用户体验。
    本文要讲解的正是为程序添加这样的一个右下角托盘,并实现显示程序主窗口、退出程序等功能。本文分别在MFC程序和Windows应用程序中进行演示,其实,原理步骤都是相同的,只是为了方便大家理解,所以MFC程序和Windows程序都各写一个来演示。
    现在,我就把程序实现的过程整理成文档,分享给大家。
    函数介绍Shell_NotifyIcon 函数
    主要用于向任务栏的状态栏发送一个消息。
    函数声明
    BOOL Shell_NotifyIcon( DWORD dwMessage, PNOTIFYICONDATA lpdata);
    参数

    dwMessage为输入参数,传递发送的消息,表明要执行的操作。可选的值如下:NIM_ADD向托盘区域添加一个图标。此时第二个参数lpdata指向的NOTIFYICONDATA结构体中的hWnd和uID成员用来标示这个图标,以便以后再次使用Shell_NotifyIcon对此图标操作。NIM_DELETE删除托盘区域的一个图标。此时第二个参数lpdata指向的NOTIFYICONDATA结构体中的hWnd和uID成员用来标示需要被删除的这个图标。NIM_MODIFY修改托盘区域的一个图标。此时第二个参数lpdata指向的NOTIFYICONDATA结构体中的hWnd和uID成员用来标示需要被修改的这个图标。NIM_SETFOCUSVersion 5.0. 设置焦点。比如当用户操作托盘图标弹出菜单,而有按下ESC键将菜单消除后,程序应该使用此消息来将焦点设置到托盘图标上。NIM_SETVERSIONVersion 5.0. 设置任务栏按照第二个参数lpdata指向的NOTIFYICONDATA结构体中的uVersion成员指定的版本号来工作。此消息可以允许用户设置是否使用基于Windows2000的version 5.0的风格。uVersion的缺省值为0,默认指明了使用原始Windows 95图标消息风格。具体这两者的区别请参考msdn中的Shell_NotifyIcon函数说明的Remarks。lpdata为输入参数,是指向NOTIFYICONDATA结构体的指针,结构体内容用来配合第一个参数wMessage进行图标操作。
    返回值

    如果图标操作成功返回TRUE,否则返回FALSE。

    NOTIFYICONDATA 结构体
    结构体声明
    typedef struct _NOTIFYICONDATA { DWORD cbSize; HWND hWnd; UINT uID; UINT uFlags; UINT uCallbackMessage; HICON hIcon; TCHAR szTip[64]; DWORD dwState; DWORD dwStateMask; TCHAR szInfo[256]; union { UINT uTimeout; UINT uVersion; }; TCHAR szInfoTitle[64]; DWORD dwInfoFlags; GUID guidItem;} NOTIFYICONDATA, *PNOTIFYICONDATA;
    成员

    cbSize:结构体的大小,以字节为单位。hWnd:窗口的句柄。窗口用来接收与托盘图标相关的消息。Shell_NotifyIcon函数调用时,hWnd和uID成员用来标示具体要操作的图标。uID:应用程序定义的任务栏图标的标识符。Shell_NotifyIcon函数调用时,hWnd和uID成员用来标示具体要操作的图标。通过将多次调用,你可以使用不同的uID将多个图标关联到一个窗口。hWnd。uFlags:此成员表明具体哪些其他成员为合法数据(即哪些成员起作用)。此成员可以为以下值的组合:NIF_ICON:hIcon成员起作用。NIF_MESSAGE:uCallbackMessage成员起作用。NIF_TIP:szTip成员起作用。NIF_STATE:dwState和dwStateMask成员起作用。NIF_INFO:使用气球提示代替普通的工具提示框。szInfo, uTimeout, szInfoTitle和dwInfoFlags成员起作用。NIF_GUID:保留。uCallbackMessage:应用程序定义的消息标示。当托盘图标区域发生鼠标事件或者使用键盘选择或激活图标时,系统将使用此标示向由hWnd成员标示的窗口发送消息。消息响应函数的wParam参数标示了消息事件发生的任务栏图标,lParam参数根据事件的不同,包含了鼠标或键盘的具体消息,例如当鼠标指针移过托盘图标时,lParam将为WM_MOUSEMOVE。hIcon:增加、修改或删除的图标的句柄。注意,windows不同版本对于图标有不同要求。Windows XP可支持32位。szTip:指向一个以\0结束的字符串的指针。字符串的内容为标准工具提示的信息。包含最后的\0字符,szTip最多含有64个字符。对于Version 5.0 和以后版本,szTip最多含有128个字符(包含最后的\0字符)。dwState:Version 5.0,图标的状态,有两个可选值,如下:NIS_HIDDEN:图标隐藏NIS_SHAREDICON:图标共享dwStateMask:Version 5.0. 指明dwState成员的那些位可以被设置或者访问。比如设置此成员为NIS_HIDDEN,将导致只有hidden状态可以被获取。szInfo:Version 5.0. 指向一个以\0结束的字符串的指针。字符串的内容为气球提示内容。最多含有255个字符。如果要移除已经存在的气球提示信息,设置uFlags成员为NIF_INFO,同时将szInfo设为空。uTimeout:和uVersion成员为联合体。uTimeout表示气球提示超时的时间,单位为毫秒,此时间后气球提示将消失。系统默认气球提示的超时时间最小值为10秒,最大值为30秒。如果设置的uTimeout的值小于10将设置最小值,如果大于30将设置最大值。将超时时间分为最大最小两种,是因为解决不同图标的气球提示同时弹出的问题,详细内容请参考MSDN中NOTIFYICONDATA结构体说明的remarks。uVersion:Version 5.0. 和uTimeout成员为联合体。用来设置使用Windows 95 还是 Windows 2000风格的图标消息接口。szInfoTitle:Version 5.0. 指向一个以\0结束的字符串的指针。字符串的内容为气球提示的标题。此标题出现在气球提示框的上部,最多含有63个字符。dwInfoFlags:Version 5.0. 设置此成员用来给气球提示框增加一个图标。增加的图标出现在气球提示标题的左侧,注意如果szInfoTitle成员设为空字符串,则图标也不会显示。guidItem:Version 6.0. 保留。

    实现过程无论是对MFC程序还是Windows应用程序,实现的步骤和原理都是一样的。
    1. 设置 NOTIFYICONDATA首先,我们需要对托盘图标结构体 NOTIFYICONDATA 进行设置,设置的内容,就是我们想要托盘图标显示的内容。我们主要是对 NOTIFYICONDATA 的cbSize、hWnd、uID、uFlags、uCallbackMessage、hIcon以及szTip成员进行设置。
    其中,cbSize表示 NOTIFYICONDATA 结构体的大小;hWnd表示和托盘图标相关联的程序窗体的句柄,这个值需要被指定,因为窗口用来接收与托盘图标相关的消息;uID表示应用程序定义的任务栏图标的标识符,可以使用不同的uID将多个图标关联到一个窗口;uFlags表示哪些结构体成员起作用,NIF_ICON则hIcon成员起作用,NIF_MESSAGE则uCallbackMessage成员起作用,NIF_TIP则szTip成员起作用,所以下面我们还需要对hIcon、uCallbackMessage以及szTip成员赋值;hIcon表示托盘显示的图标句柄;uCallbackMessage表示托盘的消息类型;szTip表示托盘的提示信息。
    NOTIFYICONDATA notifyIconData = {0};::RtlZeroMemory(&notifyIconData, sizeof(notifyIconData));notifyIconData.cbSize = sizeof(NOTIFYICONDATA);notifyIconData.hWnd = hWnd;notifyIconData.uID = IDI_ICON1;notifyIconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;notifyIconData.uCallbackMessage = WM_MYTASK;notifyIconData.hIcon = ::LoadIcon(::GetModuleHandle(NULL), (LPCSTR)IDI_ICON1);::lstrcpy(notifyIconData.szTip, "This Is A Notify Tip.");
    2. 增加托盘显示然后,我们开始调用 Shell_NotifyIcon 函数按照上面 NOTIFYICONDATA 设置新增托盘。其中,NIM_ADD表示向托盘区域添加一个图标。notifyIconData就是上面设置的 NOTIFYICONDATA 结构体变量。
    // 增加托盘::Shell_NotifyIcon(NIM_ADD, &notifyIconData);
    3. 托盘消息处理函数经过上面两步操作,我们就可以成功为程序在窗口右下角增加一个托盘图标显示。但是,我们还需要让托盘图标对一些操作做出响应,而不是只是显示而已。例如,我们鼠标右击托盘图标的时候,显示菜单栏。
    根据上面对 NOTIFYICONDATA 结构体成员 uCallbackMessage 介绍中知道 ,当托盘图标区域发生鼠标事件或者使用键盘选择或激活图标时,系统将使用此标示向由hWnd成员标示的窗口发送消息。消息响应函数的wParam参数标示了消息事件发生的任务栏图标,lParam参数根据事件的不同,包含了鼠标或键盘的具体消息,例如当鼠标指针移过托盘图标时,lParam将为WM_MOUSEMOVE。所以,我们要响应鼠标右键的操作,需要判断 lParam 是否为 WM_RBUTTONUP ,若是,弹出菜单,否则忽略操作。
    switch (lParam){// 鼠标右键弹起时case WM_RBUTTONUP: { // 弹出菜单 PopupMyMenu(); break;}default: break;}
    就这样,菜单弹出来后,我们直接响应菜单选项的消息响应函数,在对应的选项里执行相应的操作就好。
    为程序添加托盘显示的原理就是上面 3 个步骤,剩下来的就是编码实现了。
    编码实现Windows应用程序实现首先我们把对NOTIFYICONDATA结构体的设置以及添加托盘图标的操作代码都放在程序初始化WM_INITDIALOG操作里。
    然后,我们就开始在窗口消息过程函数中添加托盘消息类型WM_MYTASK,并对实现托盘消息处理响应函数。这样,在右击托盘的时候,就可以显示菜单栏了。
    窗口消息过程函数BOOL CALLBACK ProgMainDlg(HWND hWnd, UINT uiMsg, WPARAM wParam, LPARAM lParam){ if (WM_INITDIALOG == uiMsg) { // 设置托盘显示 SetNotifyIcon(hWnd); } else if (WM_CLOSE == uiMsg) { // 隐藏窗口 ::ShowWindow(hWnd, SW_HIDE); } else if (WM_MYTASK == uiMsg) { // 处理操作托盘的消息 OnTaskMsg(hWnd, wParam, lParam); } else if (WM_COMMAND == uiMsg) { if (ID_EXIT == wParam) { // 托盘菜单 退出 ::EndDialog(hWnd, NULL); } else if (ID_SHOW == wParam) { // 托盘菜单 显示主窗口 ::ShowWindow(hWnd, SW_SHOW); } } return FALSE;}
    设置托盘显示// 设置托盘显示void SetNotifyIcon(HWND hWnd){ NOTIFYICONDATA notifyIconData = {0}; ::RtlZeroMemory(&notifyIconData, sizeof(notifyIconData)); notifyIconData.cbSize = sizeof(NOTIFYICONDATA); notifyIconData.hWnd = hWnd; notifyIconData.uID = IDI_ICON1; notifyIconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; notifyIconData.uCallbackMessage = WM_MYTASK; notifyIconData.hIcon = ::LoadIcon(::GetModuleHandle(NULL), (LPCSTR)IDI_ICON1); ::lstrcpy(notifyIconData.szTip, "This Is A Notify Tip."); // 增加托盘 ::Shell_NotifyIcon(NIM_ADD, &notifyIconData);}
    托盘消息处理函数// 处理操作托盘的消息LRESULT OnTaskMsg(HWND hWnd, WPARAM wParam, LPARAM lParam){ switch (lParam) { // 鼠标右键弹起时,弹出菜单 case WM_RBUTTONUP: { // 弹出菜单 PopupMyMenu(hWnd); break; } default: break; } return 0;}
    弹出菜单栏// 弹出菜单栏void PopupMyMenu(HWND hWnd){ POINT p; ::GetCursorPos(&p); HMENU hMenu = ::LoadMenu(::GetModuleHandle(NULL), (LPCSTR)IDR_MENU1); HMENU hSubMenu = ::GetSubMenu(hMenu, 0); ::TrackPopupMenu(hSubMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, p.x, p.y, 0, hWnd, NULL); ::DestroyMenu(hSubMenu);}
    MFC程序实现和上面的Windows应用程序一样,首先我们把对NOTIFYICONDATA结构体的设置以及添加托盘图标的操作代码都放在程序初始化,对于MFC来说,初始化消息响应函数是主对话框类的OnInitDialog函数。我们把托盘设置这部分代码,放在函数OnInitDialog中即可。
    然后,我们开始对为自定义消息类型 WM_MYTASK 创建自定义消息响应函数 OnTaskMsg。创建自定义消息响应函数操作如下:

    声明消息类型 WM_MYTASK。
    #define WM_MYTASK (WM_USER + 100)
    声明消息响应函数,函数名称可以任意,但是返回值类型和参数类型是固定的。
    // 托盘消息处理函数 LRESULT OnTaskMsg(WPARAM wParam, LPARAM lParam);
    在主对话框的窗口消息响应列表中,为上述自定义的消息类型和消息响应函数进行关联。
    BEGIN_MESSAGE_MAP(CNotifyIcon_MFC_TestDlg, CDialogEx) … …(省略) ON_MESSAGE(WM_MYTASK, OnTaskMsg) END_MESSAGE_MAP()

    这样,我们就可以直接定义响应函数OnTaskMsg就可以了。
    接下来的操作和代码,基本上和Windows应用程序中的代码是一样的,所以,就给出代码了。若有什么问题,可以直接参考本文对应的程序代码即可。
    程序测试我们直接运行程序,便可以看到窗口右下角有托盘图标显示,然后我们鼠标右击图标,便成功弹出菜单栏,点击菜单栏选项,成功实现相应的操作。

    总结这个程序原理上不是很复杂,但是实现上面,由于Windows应用程序和MFC程序框架上不同,所以,要注意它们的区别,特别是MFC的自定义消息响应函数。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2018-11-07 11:23:55
  • 编程实现MFC程序窗口一运行立马隐藏

    背景MFC程序在启动显示窗口之前,就已经做了很多关于界面初始化的操作。如果对WIN32 API函数较为熟悉的同学就会认为,让窗口隐藏,直接调用 ShowWindow 函数,将窗口设置为隐藏就可以了。是的,ShowWindow 函数可以隐藏窗口没错。但是,这个函数要成功实现的窗口隐藏有一个前提条件,就是窗口界面初始化已经完成,而且窗口界面你已经显示完全了。这时,再调用 ShowWindow 函数去隐藏窗口,这是可以的。
    但是,本文要实现的是,窗口程序一运行就已经开始隐藏的功能,而不是显示后隐藏。也就是说,在窗口界面初始化阶段,窗口界面还没有显示的时候,就已经开始隐藏界面了。这样的隐藏,使得程序运行无声无息。
    现在,我们把这个程序的实现过程整理成文档,分享给大家。
    函数介绍MoveWindow 函数
    改变指定窗口的位置和大小。对子窗口来说,位置和大小取决于父窗口客户区的左上角;对于Owned窗口,位置和大小取决于屏幕左上角。
    函数声明
    BOOL MoveWindow( HWND hWnd, int X, int Y, int nWidth, int nHeight, BOOL bRepaint );
    参数

    hWnd [in]窗口的句柄。X [in]窗口左侧的新位置。Y [in]窗口顶部的新位置。nWidth [in]窗口的新宽度。nHeight [in]窗口的新高度。bRepaint [in]指示窗口是否要重画。 如果此参数为TRUE,窗口将收到一条消息。 如果参数为FALSE,则不会发生任何重画。 这适用于客户端区域,非客户区域(包括标题栏和滚动条),父窗口的任何部分由于移动子窗口而被覆盖。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    GetWindowLong 函数
    获取有关指定窗口的信息。 函数也获得在额外窗口内存中指定偏移位地址的32位度整型值。
    函数声明
    LONG GetWindowLong( HWND hWnd, int nlndex );
    参数

    hWnd目标窗口的句柄。它可以是窗口句柄及间接给出的窗口所属的窗口类。
    nlndex需要获得的相关信息的类型。要获得任意其他值,指定下列值之一:




    VALUE
    MEANING




    GWL_EXSTYLE
    获得扩展窗口风格。


    GWL_HINSTANCE
    获得应用实例的句柄


    GWL_HWNDPARENT
    如果父窗口存在,获得父窗口句柄


    GWL_ID
    获得窗口标识


    GWL_STYLE
    获得窗口风格


    GWL_USERDATA
    获得与窗口有关的32位值。每一个窗口均有一个由创建该窗口的应用程序使用的32位值


    GWL_WNDPROC
    获得窗口过程的地址,或代表窗口过程的地址的句柄。必须使用CallWindowProc函数调用窗口过程



    hWnd参数为对话框句柄时,还可用下列值:



    VALUE
    MEANING




    DWL_DLGPROC
    获得对话框过程的地址,或一个代表对话框过程的地址的句柄。必须使用函数CallWindowProc来调用对话框过程


    DWL_MSGRESULT
    获得在对话框过程中一个消息处理的返回值


    DWL_USER
    获得应用程序私有的额外信息,例如一个句柄或指针



    返回值

    如果函数成功,返回值是所需的32位值;如果函数失败,返回值是0。若想获得更多错误信息请调用 GetLastError函数。

    SetWindowLong 函数
    用来改变指定窗口的属性.函数也将指定的一个32位值设置在窗口的额外存储空间的指定偏移位置。
    函数声明
    LONG SetWindowLong( HWND hWnd, // handle to window int nlndex, // offset of value to set LONG dwNewLong // new value);
    参数

    hWnd窗口句柄及间接给出的窗口所属的类。nlndex设置相关信息的类型。要获得任意其他值,指定下列值之一(和上面 GetWindowLong 函数的 nIndex 值相同)。dwNewLong指定的替换值。
    返回值

    如果函数成功,返回值是指定的32位整数的原来的值。如果函数失败,返回值为0。若想获得更多错误信息,请调用GetLastError函数。

    实现原理窗口移动到显示屏幕之外要设置程序窗口显示的位置和大小,我们可以直接调用 MoveWindow 函数,设置移动后窗口显示的位置坐标、窗口大小。这时,我们只需要将窗口大小都设为 0 ,位置坐标都设为-1000(屏幕位置坐标以屏幕左上角为起点),这样窗口就在屏幕之外显示了。
    隐藏任务栏程序图标其中,我们先来了解 GWL_EXSTYLE 窗口拓展属性中的两个值的含义,这是要理解隐藏任务栏比较关键的一步:
    一是 WS_EX_APPWINDOW,表示当窗口可见时,将一个顶层窗口放置到任务条上;
    二是 WS_EX_TOOLWINDOW,表示创建工具窗口,即窗口是一个游动的工具条。工具窗口的标题条比一般窗口的标题条短,并且窗口标题以小字体显示。工具窗口不在任务栏里显示,当用户按下alt+Tab键时工具窗口不在对话框里显示。如果工具窗口有一个系统菜单,它的图标也不会显示在标题栏里,但是,可以通过点击鼠标右键或Alt+Space来显示菜单。
    由上面 WS_EX_APPWINDOW 和 WS_EX_TOOLWINDOW 可知,我们要想实现任务栏程序的隐藏。那么,就要把 WS_EX_APPWINDOW 值去掉,然后再增加值 WS_EX_TOOLWINDOW。所以:

    首先调用 GetWindowLong 函数获取窗口 GWL_EXSTYLE 属性的值 dwOldStyle;
    然后,去掉 WS_EX_APPWINDOW 值,即
    dwOldStyle & (~WS_EX_APPWINDOW)
    接着,增加 WS_EX_APPWINDOW 值,即
    dwNewStyle = dwNewStyle | WS_EX_TOOLWINDOW;
    最后,调用 SetWindowLong 将新的 GWL_EXSTYLE 属性的值重新设置,这样就完成了。

    编码实现// 隐藏窗口void HideWindow(HWND hWnd){ // 把窗口移动到屏幕显示之外, 窗口大小都设为 0 ::MoveWindow(hWnd, -1000, -1000, 0, 0, FALSE); // 先获取原来窗口的拓展风格 DWORD dwOldStyle = ::GetWindowLong(hWnd, GWL_EXSTYLE); // 设置新属性, 隐藏任务栏程序图标 DWORD dwNewStyle = dwOldStyle & (~WS_EX_APPWINDOW); dwNewStyle = dwNewStyle | WS_EX_TOOLWINDOW; ::SetWindowLong(hWnd, GWL_EXSTYLE, dwNewStyle);}
    总结我们隐藏窗口的思路就两点:一是把窗口的显示位置坐标移到屏幕之外,这样窗口显示的时候,在屏幕之内是看不到的;二是把任务栏的程序图标隐藏,因为第一步操作会因为任务栏下的图标露出马脚,所以,必须隐藏任务栏中程序的图标。这样,就可以实现一运行程序,窗口就隐藏了。
    大家要注意,虽然窗口隐藏了,我们打开任务管理器还是会看到我们程序的进程的。所以,关于进程隐藏,就是另一个不同知识点了。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2018-11-07 11:20:19
  • 基于MuPDF库实现PDF文件转换成PNG格式图片

    背景之所以会接触MuPDF是因为,有位群友在Q群里提问,如何将PDF保存为.PNG图片格式。我一看到这个问题,就蒙了,因为我没有接触过类似的项目或程序。但是,作为一群之主的我,还是要给初学者一个答复的,所以便去网上搜索了相关信息,才了解到有MuPDF开源库的存在。
    MuPDF是一种轻量级的PDF,XPS和电子书阅读器。由各种平台的软件库,命令行工具和查看器组成。支持许多文档格式,如PDF,XPS,OpenXPS,CBZ,EPUB和FictionBook 2。
    后来,自己就根据网上搜索到的一些资料,实现了基于MuPDF库将PDF指定页转换成PNG格式图片的小程序。现在,我就把程序的实现思路和过程写成文档,分享给大家。
    实现思路对于MuPDF库的源码下载以及编译过程,可以参考本站的《使用VS2013编译MuPDF库》这篇文章。
    其实,在MuPDF库中就提供了这一个功能:将PDF指定页转换成PNG格式图片,所以,我们直接编译MuPDF提供的代码就可以了。
    示例程序代码位于MuPDF库源码的“docs”目录下的“example.c”文件,我们只需使用VS2013创建一个空项目,然后把“example.c”文件导入项目中,接着将“include”目录中的头文件以及编译出来的libmupdf.lib、libmupdf-js-none.lib、libthirdparty.lib导入文件中即可。
    其中,“example.c”中主要的函数就是 render 函数,我们主要是把参数传进render函数,就可以把pdf转换成png图片了。
    对于“example.c”的代码可以不用修改,直接编译运行即可。但是,为了方便演示,我们还是对“example.c”中的 main 函数进行修改。
    编码实现以下是“example.c”文件中 render 函数的源码:
    void render(char *filename, int pagenumber, int zoom, int rotation){ // Create a context to hold the exception stack and various caches. fz_context *ctx = fz_new_context(NULL, NULL, FZ_STORE_UNLIMITED); // Open the PDF, XPS or CBZ document. fz_document *doc = fz_open_document(ctx, filename); // Retrieve the number of pages (not used in this example). int pagecount = fz_count_pages(doc); // Load the page we want. Page numbering starts from zero. fz_page *page = fz_load_page(doc, pagenumber - 1); // Calculate a transform to use when rendering. This transform // contains the scale and rotation. Convert zoom percentage to a // scaling factor. Without scaling the resolution is 72 dpi. fz_matrix transform; fz_rotate(&transform, rotation); fz_pre_scale(&transform, zoom / 100.0f, zoom / 100.0f); // Take the page bounds and transform them by the same matrix that // we will use to render the page. fz_rect bounds; fz_bound_page(doc, page, &bounds); fz_transform_rect(&bounds, &transform); // Create a blank pixmap to hold the result of rendering. The // pixmap bounds used here are the same as the transformed page // bounds, so it will contain the entire page. The page coordinate // space has the origin at the top left corner and the x axis // extends to the right and the y axis extends down. fz_irect bbox; fz_round_rect(&bbox, &bounds); fz_pixmap *pix = fz_new_pixmap_with_bbox(ctx, fz_device_rgb(ctx), &bbox); fz_clear_pixmap_with_value(ctx, pix, 0xff); // A page consists of a series of objects (text, line art, images, // gradients). These objects are passed to a device when the // interpreter runs the page. There are several devices, used for // different purposes: // // draw device -- renders objects to a target pixmap. // // text device -- extracts the text in reading order with styling // information. This text can be used to provide text search. // // list device -- records the graphic objects in a list that can // be played back through another device. This is useful if you // need to run the same page through multiple devices, without // the overhead of parsing the page each time. // Create a draw device with the pixmap as its target. // Run the page with the transform. fz_device *dev = fz_new_draw_device(ctx, pix); fz_run_page(doc, page, dev, &transform, NULL); fz_free_device(dev); // Save the pixmap to a file. fz_write_png(ctx, pix, "out.png", 0); // Clean up. fz_drop_pixmap(ctx, pix); fz_free_page(doc, page); fz_close_document(doc); fz_free_context(ctx);}
    程序测试我们修改 main 函数直接调用上述函数接口, main 函数为:
    int main(int argc, char **argv){ // 文件路径 char filename[MAX_PATH] = "C:\\Users\\DemonGan\\Desktop\\test.pdf"; // 转换的页码数 int pagenumber = 1; // 缩放比例 int zoom = 100; // 旋转角度 int rotation = 0; // 处理 render(filename, pagenumber, zoom, rotation); system("pause"); return 0;}
    直接运行程序,目录下有 out.png 图片生成,打开图片查看内容,发现是 test.pdf 的第一页内容,所以转换成功。
    总结这个程序的实现,自己可以不用写代码就可以完成。因为MuPDF已经把接口都封装好了,而且也有示例程序可以直接调用。如果想要把界面做得更有好些,可以把程序写成界面程序,然后直接调用现在的这个 render 函数接口,进行转换即可。
    3 回答 2018-11-06 22:31:04
显示 60 到 70 ,共 10 条
eject