分类

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

文章列表

  • 【Cocos Creator实战教程(14)】——打包游戏(修改图标)

    1. 步骤我们打包一个Android的Apk

    模板选择binary可以快速打包,Portrait竖屏,Upside Down倒屏,Landscape Left左右横屏,Landscape Right右左横屏
    我们先点击构建

    构建完成后我们找到构建的目录(就是上面的build文件夹)
    build/jsb-binary/frameworks/runtime-src/proj.android/res/

    将里面的图标换成我们的女主角

    替换完成后再点击编译
    进入下面的目录就可以看见我们的Apk文件了

    2. 总结到这里,游戏差不多就做完了,接下来我会讲几个游戏中的算法。
    同志们一定要记住多学多练,另外有什么想做的游戏也可以留言给我呀。
    本教程部分资源来源于网络。
    2 回答 2018-12-01 20:29:47
  • 【Cocos Creator实战教程(13)】——播放声音

    1. 相关知识点1.1 使用 AudioSource 组件播放创建一个空节点,在这个空节点上,添加一个 其他组件 -> AudioSource在脚本上预设好 AudioSource,并且根据实际需求,完善脚本的对外接口,如下:
    cc.Class({ properties: { audioSource: { type: cc.AudioSource, default: null }, }, play: function () { this.audioSource.play(); }, pause: function () { this.audioSource.pause(); },});
    1.2 使用 AudioEngine 播放在脚本内定义一个 audioClip 资源对象,如下示例中 properties 对象内。直接使用 cc.audioEngine.play(audio, loop, volume); 播放。如下示例中 onLoad 中。
    cc.Class({ properties: { audio: { default: null, type: cc.AudioClip } }, onLoad: function () { this.current = cc.audioEngine.play(this.audio, false, 1); }, onDestroy: function () { cc.audioEngine.stop(this.current); }});
    AudioEngine 播放的时候,需要注意这里的传入的是一个完整的 AudioClip 对象(而不是 url)。 所以我们不建议在 play 接口内直接填写音频的 url 地址,而是希望大家先定义一个 AudioClip,然后在编辑器内将音频拖拽过来。
    2. 步骤播放声音有两种方式
    2.1 组件AudioSourceplay ( )播放音频剪辑。stop ( )停止当前音频剪辑。pause ( )暂停当前音频剪辑。resume ( )恢复播放。rewind ( )从头开始播放。
    2.2 声音系统//背景音乐,循环cc.audioEngine.playMusic(source);cc.audioEngine.stopMusic(source);//短音效cc.audioEngine.playEffect(source);cc.audioEngine.stopEffect(source);
    上面的第一种方法原生平台有很多Bug,所以我们的游戏都用的第二种方法播放声音
    3.总结加上音效,让游戏锦上添花
    本教程部分资源来源于网络。
    2 回答 2018-12-01 20:12:02
  • 【Cocos Creator实战教程(12)】——存储与读取数据

    1. 相关知识点我们在游戏中通常需要存储用户数据,如音乐开关、显示语言等,如果是单机游戏还需要存储玩家存档。 Cocos Creator 中我们使用 cc.sys.localStorage 接口来进行用户数据存储和读取的操作。
    1.1 存储数据cc.sys.localStorage.setItem(key, value)
    上面的方法需要两个参数,用来索引的字符串键值 key,和要保存的字符串数据 value。
    假如我们要保存玩家持有的金钱数,假设键值为 gold:
    cc.sys.localStorage.setItem('gold', 100);
    对于复杂的对象数据,我们可以通过将对象序列化为 JSON 后保存:
    userData = { name: 'Tracer', level: 1, gold: 100};cc.sys.localStorage.setItem('userData', JSON.stringify(userData));
    1.2 读取数据cc.sys.localStorage.getItem(key)
    和 setItem 相对应,getItem 方法只要一个键值参数就可以取出我们之前保存的值了。对于上文中储存的用户数据:
    var userData = JSON.parse(cc.sys.localStorage.getItem('userData'));
    1.3 移除键值对当我们不再需要一个存储条目时,可以通过下面的接口将其移除:
    cc.sys.localStorage.removeItem(key)
    1.4 数据加密对于单机游戏来说,对玩家存档进行加密可以延缓游戏被破解的时间。要加密存储数据,只要在将数据通过 JSON.stringify 转化为字符串后调用你选中的加密算法进行处理,再将加密结果传入 setItem 接口即可。
    您可以搜索并选择一个适用的加密算法和第三方库,比如 encryptjs, 将下载好的库文件放入你的项目,存储时:
    var encrypt=require('encryptjs');var secretkey= 'open_sesame'; // 加密密钥var dataString = JSON.stringify(userData);var encrypted = encrypt.encrypt(dataString,secretkey,256);cc.sys.localStorage.setItem('userData', encrypted);
    读取时:
    var cipherText = cc.sys.localStorage.getItem('userData');var userData=JSON.parse(encrypt.decrypt(cipherText,secretkey,256));
    注意 数据加密不能保证对用户档案的完全掌控,如果您需要确保游戏存档不被破解,请使用服务器进行数据存取。
    2. 步骤2.1 存储数据cc.sys.localStorage.setItem('bestRunScore', this.bestRunScore);cc.sys.localStorage.setItem('bestJumpScore', this.bestJumpScore);
    2.2 读取数据cc.sys.localStorage.getItem("bestRunScore");cc.sys.localStorage.getItem("bestJumpScore");
    就是两个封装的方法,存储的方式是键值对
    2.3 Menu脚本也需要修改一下Menu.js
    ...onLoad: function () { this.record = cc.find("Record").getComponent("Record"); this.runScore.string = "你最远跑了" + this.record.bestRunScore+ "m"; this.jumpScore.string = "你最高跳了" + this.record.bestJumpScore+ "m"; ...},...
    3. 总结在现实生活中,我们经常重要数据传回服务器,而一些不重要数据则存储在本地。而且现在json数据格式很普遍,php等都可以使用json。
    本教程部分素材来源于网络。
    2 回答 2018-12-01 20:11:07
  • 【Cocos Creator实战教程(11)】——跨场景访问节点

    1. 相关知识点从这节课开始,我们就要回到之前制作的游戏上了。还记得之前做的菜单场景吗?

    中间有一个记录的label我们一直没理她,今天我们就来翻她的牌子
    我们每次游戏结束时都会有一个分数,这个分数变量在相应的游戏场景里,我们想要的效果时:当返回菜单时,我们要把这个分数变量带回来,但当场景销毁时,其中的所有节点都会随之消失
    这里就要引出另一个重要的知识点,同学们拿笔记一下(没带笔的前后桌借一下)
    不会跟随场景销毁的节点——常驻节点
    2. 步骤我们回到第一个Load场景,添加一个根节点(必须是根节点哦)Record

    编写脚本Record.js
    cc.Class({ extends: cc.Component, properties: { bestRunScore: 0, bestJumpScore: 0, }, onLoad: function () { cc.game.addPersistRootNode(this.node); var bestRunScore = cc.sys.localStorage.getItem("bestRunScore"); if(bestRunScore){ this.bestRunScore = bestRunScore; } var bestJumpScore = cc.sys.localStorage.getItem("bestJumpScore"); if(bestRunScore){ this.bestJumpScore = bestJumpScore; } }, updateRunScore: function(score){ if(score > this.bestRunScore){ this.bestRunScore = score; } }, updateJumpScore: function(score){ if(score > this.bestJumpScore){ this.bestJumpScore = score; } }, save(){ cc.sys.localStorage.setItem('bestRunScore', this.bestRunScore); cc.sys.localStorage.setItem('bestJumpScore', this.bestJumpScore); },});
    我们在onLoad方法里将Record节点变成游戏的常驻节点
    cc.game.addPersistRootNode(this.node);
    销毁的方法是
    cc.game.removePersistRootNode(node);
    游戏中的常驻节点,在切换场景时不会销毁,所以我们可以把一些需要跨场景访问的方法和变量添加到常驻节点的脚本里
    我们切换到Menu场景,惊奇的发现,那个常驻节点Record并没有出现!

    这是因为常驻节点只是逻辑上的,并不会在其他场景层级管理器里出现,这就要用另一个找节点的方法了
    cc.find();
    接着我们修改两个Game的stopGame方法
    Game.js
    stopGame: function(){ cc.director.getCollisionManager().enabled = false; this.gameOverMenu.active = true; this.overScore.string = this.score+"m"; //存储数据 cc.find("Record").getComponent("Record").updateRunScore(this.score);},
    Game2.js
    stopGame: function(){ cc.director.getCollisionManager().enabled = false; this.gameOverMenu.getChildByName('OverScore').getComponent(cc.Label).string = this.score; this.gameOverMenu.active = true; //存储数据 cc.find("Record").getComponent("Record").updateJumpScore(this.score);},
    3. 总结跨场景访问节点主要用于保存游戏状态,所以很多时候要配合着存储数据使用。下节课我们就来介绍存储数据,不见不散~~
    本教程部分资源来源于网络。
    1 回答 2018-12-01 16:49:18
  • 编程使用WMI

    背景
    WMI出现至今已经二十多年了,但很多人对它并不熟悉。知道它很好很强大,但不知道它从哪里来,怎么工作,使用范围是什么?
      WMI有一组API。我们不管使用VBScript、PowerShell脚本还是利用C#的来访问WMI的类库,都是因为WMI向外暴露的一组API。这些API是在系统安装WMI模块的时候安装的,通过他们我们能够能拿到我们想要的类。
      WMI有一个存储库。尽管WMI的多数实例数据都不存储在WMI中,但是WMI确实有一个存储库,用来存放提供程序提供的类信息,或者称为类的蓝图或者Schema。
      WMI有一个Service。WMI总是能够响应用户的访问,那是因为它有一个一直运行的Windows服务,名字叫Winmgmt。停止这个服务,所有对WMI的操作都将没有反应。
      WMI是可扩展的。人人都知道WMI能干很多事情,读取本机硬盘信息、读取远程计算机的用户信息、读取域用户信息等等。基本上,你能想到的获取或者更改资源的操作,它都能干。可谓吃得少,干得多。它为什么这么能干呢?这基于WMI的可扩展性。WMI对资源的操作,不是它自己实现了什么方法,而完全取决于向它注册的提供程序。
      WMI是管理员日常必备的强大工具之一,是脚本伴侣。当然也可以把一个大型系统建立在WMI以及WMI的提供程序之上。
    WMI的全称是Windows Management Instrumentation,即Windows管理工具。它是Windows操作系统中管理数据和操作的基础模块。我们可以通过WMI脚本或者应用程序去管理本地或者远程计算机上的资源。对于VC和汇编程序员,想获取诸如CPU序列号和硬盘序列号等信息是非常容易的。但是对于VB以及其他一些脚本语言,想尝试获取系统中一些硬件信息可能就没那么容易了。微软为了能达到一种通用性目的(遵守某些行业标准),设计了WMI。它提供了一个通过操作系统、网络和企业环境去管理本地或远程计算机的统一接口集。应用程序和脚本语言使用这套接口集去完成任务,而不是直接通过Windows API。可能有人要问,为什么不让设计的脚本直接在底层使用Windows API,而非要弄个新的技术呢?原因是在目前Windows API中,有些是不支持远程调用或者脚本调用的。这样通过统一模型的WMI,像VB和脚本语言就可以去访问部分系统信息了。但是并不是所有脚本语言都可以使用WMI技术:它要支持ActiveX技术。

    WMI通常是被脚本所调用的,不过也对。对于WMI能做的操作,我们也完全可以通过VS去调用Win32 API去实现。但是,本文要讲的是使用VS调用WMI提供的接口去获取系统的信息。那么,VS中怎么使用WMI呢?接下来,我就把我所了解的知识分享给大家。
    函数介绍CoInitializeEx 函数
    为当前线程初始化COM库并设置并发模式 。
    函数声明
    HRESULT CoInitializeEx( void * pvReserved, DWORD dwCoInit);
    参数

    pvReserved系统 保留的参数,必须传入 NULL。dwCoInit该标示指明基于当前线程的并发模式和初始化选项。该参数是 COINIT 枚举类型,传入参数时候,除了COINIT_APARTMENTTHREADED 和COINIT_MULTITHREAD ED标记外,其余的标记可以组合使用。
    返回值

    S_OK :COM库初始化成功。S_FALSE :当前线程上,COM库已经被初始化。RPC_E_CHANGED_MODE :COM库已经被初始化且传入参数设置的并发模式和本次不同。

    CoInitializeSecurity 函数
    注册安全性并设置进程的默认安全性值。
    函数声明
    HRESULT CoInitializeSecurity( _In_opt_ PSECURITY_DESCRIPTOR pSecDesc, _In_ LONG cAuthSvc, _In_opt_ SOLE_AUTHENTICATION_SERVICE *asAuthSvc, _In_opt_ void *pReserved1, _In_ DWORD dwAuthnLevel, _In_ DWORD dwImpLevel, _In_opt_ void *pAuthList, _In_ DWORD dwCapabilities, _In_opt_ void *pReserved3);
    参数

    pSecDesc [in]服务器将用于接收呼叫的访问权限。cAuthSvc [in]asAuthSvc参数中的条目计数。只有当服务器调用CoInitializeSecurity时,此参数才被COM使用。如果此参数为0,则不会注册认证服务,并且服务器无法接收安全呼叫。值为-1表示COM选择要注册的身份验证服务,如果是这种情况,则asAuthSvc参数必须为NULL。但是,如果参数为-1,则服务器将不会选择Schannel作为身份验证服务。asAuthSvc [in]一组服务器愿意用来接收呼叫的认证服务。pReserved1 [in]此参数是保留的,必须为NULL。dwAuthnLevel [in]进程的默认身份验证级别。dwImpLevel [in]代理的默认模拟级别。此参数的值仅在进程为客户端时使用。pAuthList [in]指向SOLE_AUTHENTICATION_LIST的指针,它是一个SOLE_AUTHENTICATION_INFO结构的数组。dwCapabilities [in]通过设置一个或多个EOLE_AUTHENTICATION_CAPABILITIES值指定的客户端或服务器的附加功能。pReserved3 [in]此参数是保留的,必须为NULL。
    返回值

    S_OK :COM库初始化成功。RPC_E_TOO_LATE:CoInitializeSecurity 已经被调用。E_OUT_OF_MEMORY:内存不足。

    CoCreateInstance 函数
    用指定的类标识符创建一个COM对象,用指定的类标识符创建一个未初始化的对象。
    函数声明
    STDAPI CoCreateInstance( REFCLSID rclsid, //创建的Com对象的类标识符(CLSID) LPUNKNOWN pUnkOuter, //指向接口IUnknown的指针 DWORD dwClsContext, //运行可执行代码的上下文 REFIID riid, //创建的Com对象的接口标识符 LPVOID * ppv //用来接收指向Com对象接口地址的指针变量);
    参数

    rclsid[in] 用来唯一标识一个对象的CLSID(128位),需要用它来创建指定对象。pUnkOuter[in] 如果为NULL, 表明此对象不是聚合式对象一部分。如果不是NULL, 则指针指向一个聚合式对象的IUnknown接口。dwClsContext[in] 组件类别. 可使用CLSCTX枚举器中预定义的值。riid[in] 引用接口标识符,用来与对象通信。ppv[out] 用来接收指向接口地址的指针变量。如果函数调用成功,*ppv包括请求的接口指针。
    返回值

    S_OK:指定的Com对象实例被成功创建。REGDB_E_CLASSNOTREG:指定的类没有在注册表中注册. 也可能是指定的dwClsContext没有注册或注册表中的服务器类型损坏。CLASS_E_NOAGGREGATION:这个类不能创建为聚合型。E_NOINTERFACE:指定的类没有实现请求的接口, 或者是IUnknown接口没有暴露请求的接口。

    CoSetProxyBlanket 函数
    设置将用于在指定代理上进行呼叫的认证信息。
    函数声明
    HRESULT CoSetProxyBlanket( _In_ IUnknown *pProxy, _In_ DWORD dwAuthnSvc, _In_ DWORD dwAuthzSvc, _In_opt_ OLECHAR *pServerPrincName, _In_ DWORD dwAuthnLevel, _In_ DWORD dwImpLevel, _In_opt_ RPC_AUTH_IDENTITY_HANDLE pAuthInfo, _In_ DWORD dwCapabilities);
    参数

    pProxy [in]要设置的代理。dwAuthnSvc [in]要使用的身份验证服务。dwAuthzSvc [in]要使用的授权服务。pServerPrincName [in]要与身份验证服务一起使用的服务器主体名称。dwAuthnLevel [in]要使用的认证级别。dwImpLevel [in]要使用的模拟级别。pAuthInfo [in]指向建立客户端身份的RPC_AUTH_IDENTITY_HANDLE值的指针。由句柄引用的结构的格式取决于认证服务的提供者。dwCapabilities [in]这个代理的功能。
    返回值

    S_OK:执行成功。E_INVALIDARG:一个或者多个参数无效。

    实现原理现在,我们来解析下在VS2013中使用WMI的原理过程:

    首先,我们要使用CoInitializeEx初始化COM组件环境。因为WMI是基于COM组件实现的,使用COM组件前,必须要进行初始化操作。
    然后,使用CoInitializeSecurity注册安全性并设置进程的默认安全性值。
    接着,使用CoCreateInstance创建一个IWbemLocator的COM对象。
    跟着,通过IWbemLocator::ConnectServer函数连接到WMI,并获取IWbemServices指针。
    然后,使用CoSetProxyBlanket设置连接的安全级别。
    接着,使用IWbemServices指针发出WMI请求,执行WQL语句。
    然后,从查询返回集中获取获取返回数据。
    进行清理工作。

    其中,WQL其实非常简单,它有如下特点:
    SELECT 属性名 FROM 类名每个WQL语句必须以SELECT开始,SELECT后跟你需要查询的属性名,也可以像SQL一样,以*表示返回所有属性值。然后,FROM关键字,即你要查询的类的名字。
    本文使用的WQL语句是:SELECT * FROM Win32_Process。
    编码实现加载WMI所需的库文件#include <comdef.h>#include <WbemIdl.h>#pragma comment(lib, "wbemuuid.lib")
    调用WIM获取进程信息int WMI_EnumProcess(){ /*******************************************/ // 初始化操作 // /*******************************************/ // 初始化COM组件 HRESULT hRes = ::CoInitializeEx(NULL, COINIT_MULTITHREADED); if (FAILED(hRes)) { printf("CoInitializeEx Error[%d]\n", hRes); return 1; } // 设置进程的安全级别 hRes = ::CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL); if (FAILED(hRes)) { printf("CoInitializeSecurity Error[%d]\n", hRes); return 2; } /*******************************************/ // 创建一个WMI命名空间连接 // /*******************************************/ // 创建一个CLSID_WbemLocation对象 IWbemLocator *pIWbemLocator = NULL; hRes = ::CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID *)(&pIWbemLocator)); if (FAILED(hRes)) { printf("CoCreateInstance Error[%d]\n", hRes); return 3; } // 使用 pIWbemLocator 连接到 "root\cimv2", 并获取 pIWbemServices IWbemServices *pIWbemServices = NULL; hRes = pIWbemLocator->ConnectServer(_bstr_t(L"ROOT\\CIMV2"), NULL, NULL, NULL, 0, NULL, NULL, &pIWbemServices); if (FAILED(hRes)) { printf("pIWbemLocator->ConnectServer Error[%d]\n", hRes); return 4; } /*******************************************/ // 设置连接的安全级别 // /*******************************************/ // 设置连接的安全级别 hRes = ::CoSetProxyBlanket(pIWbemServices, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_NONE, NULL, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE); if (FAILED(hRes)) { printf("CoSetProxyBlanket Error[%d]\n", hRes); return 5; } /*******************************************/ // 执行WQL查询代码 // /*******************************************/ // 这里是列出正在运行进程的例子 // 为了接收结果, 你必须定义一个枚举对象 IEnumWbemClassObject *pIEnumWbemClassObject = NULL; hRes = pIWbemServices->ExecQuery(bstr_t("WQL"), bstr_t("SELECT * FROM Win32_Process"), // 类似数据库中的 SQL 语句 WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, &pIEnumWbemClassObject); if (FAILED(hRes)) { printf("pIWbemServices->ExecQuery Error[%d]\n", hRes); return 6; } // 显示查询结果 IWbemClassObject *pIWbemClassObject = NULL; ULONG ulRet = 0; VARIANT varProp = { 0 }; while (pIEnumWbemClassObject) { // 获取下一个对象 hRes = pIEnumWbemClassObject->Next(WBEM_INFINITE, 1, &pIWbemClassObject, &ulRet); // 判断获取完毕结束 if (0 == ulRet) { break; } // 获取 ProcessId 字段的值 并 显示 ::VariantClear(&varProp); pIWbemClassObject->Get(L"ProcessId", 0, &varProp, NULL, NULL); printf("[%d]\t", varProp.intVal); // 获取 Name 字段的值 并 显示 ::VariantClear(&varProp); pIWbemClassObject->Get(L"Name", 0, &varProp, NULL, NULL); printf("[%ws]\n", varProp.bstrVal); } /*******************************************/ // 清理释放 // /*******************************************/ // 释放 ::VariantClear(&varProp); pIWbemServices->Release(); pIWbemLocator->Release(); ::CoUninitialize(); return 0;}
    程序测试在 main 函数中调用上述封装好的函数进行测试,main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ // 调用 WMI 去查询进程信息 WMI_EnumProcess(); system("pause"); return 0;}
    测试结果:
    运行程序,进程信息成功显示。所以测试成功。

    总结在VS中使用WMI,流程总是这么几步。要查询其他信息,也只是更改下WQL查询语句以及查询结果的显示而已。如果你学过数据库,或者了解过数据库相关的知识的话,你会发现WQL查询语句和数据SQL语句是很相似的。那是因为,我们之前提到的,WMI也有自己的数据库,也会在数据库里存储一些信息。
    参考文档WMI入门(一):什么是WMI
    WMI技术介绍和应用——WMI概述
    《Windows黑客编程技术详解》一书
    1 回答 2018-11-30 21:25:55
  • 【Cocos Creator实战教程(10)】——UI组件(4)Slider 组件

    1. 相关知识点Slider是一个滑动器组件
    1.1 Slider组件点击 属性检查器下面的添加组件按钮,然后从添加UI组件中选择Slider,即可添加Slider组件到节点上。
    1.2 Slider属性


    属性
    功能说明




    Handle
    滑动按钮部件,可以通过该按钮进行滑动调节Slider数值大小


    direction
    滑动器的方向,横向和竖向


    progress
    当前进度值,该数值的区间为0~1之间


    slideEvents
    滑动组件事件回调函数



    1.3 Slider事件



    属性
    功能说明




    target
    带有脚本组件的节点


    Component
    脚本组件的名称


    Handler
    指定一个回调函数,当Slider的事件发生的时候调用此函数。


    CustomEventData
    用户指定任意的字符串作为事件回调的最后一个参数传入



    Slider的回调有两个参数,第一个参数是Slider本身,第二个参数是CustomEventData
    1.4 详细说明Slider通常用于调节数值的UI(例如音量调节、亮度调节等),它主要的部件一个滑动按钮,该部件用于用户交互,通过该部件调节Slider的数值大小。
    通常的Slider的节点树,如图:

    2. 步骤编写脚本
    cc.Class({ extends: cc.Component, properties: { }, callback: function(slider, customEventData) { console.log("Slider的回调函数"); },});
    使用编辑器选定脚本

    3. 总结当我们要用slider调节音量时,要用到progress和direction重写回调函数,可以想想如何完成。
    本素材部分资源来源于网络。
    1 回答 2018-11-30 16:36:02
  • 【Cocos Creator实战教程(9)】——UI组件(3)Toggle 组件

    1. 相关知识点Toggle 是一个 CheckBox,当它和 ToggleGroup 一起使用的时候,可以变成 RadioButton。也就是经常用到的选择、多选按钮
    1.1 Toggle 属性


    属性
    功能说明




    isChecked
    布尔类型,如果这个设置为 true,则 check mark 组件会处于 enabled 状态,否则处于 disabled 状态。


    checkMark
    cc.Sprite 类型,Toggle 处于选中状态时显示的图片


    toggleGroup
    cc.ToggleGroup 类型, Toggle 所属的 ToggleGroup,这个属性是可选的。如果这个属性为 null,则 Toggle 是一个 CheckBox,否则,Toggle 是一个 RadioButton。


    Check Events
    列表类型,默认为空,用户添加的每一个事件由节点引用,组件名称和一个响应函数组成。详情见Toggle 事件章节



    注意:因为 Toggle 继承至 Button。
    1.2 Toggle 事件


    属性
    功能说明




    Target
    带有脚本组件的节点


    Component
    脚本组件名称


    Handler
    指定一个回调函数,当Toggle的事件发生的时候会调用此函数


    CustomEventData
    用户指定任意的字符串作为事件回调的最后一个参数传入



    Toggle事件的回调参数有两个: 第一个是Toggle本身,第二个参数是customEventData。
    1.3 Toggle详细说明Toggle的节点树:

    官方文档中说要将checkmark放到Background节点上面。
    注意: 在层级结构上面要将checkmark放到Background节点上面,或者在Background添加一个checkmark的子节点。
    1.4 ToggleContainer它并不是一个可见的UI组件,他可以用来修改一组Toggle组件的行为。当一组Toggle属于同一个ToggleContainer的时候,任何时候只能有有一个Toggle处于选中状态。(togglegroup新版已经弃用)



    属性
    功能说明




    allowSwitchOff
    如果这个值设置为true,那么toggle按钮在被点击的时候可以反复地被选中和未选中。



    2. 步骤2.1 单个toggle直接在属性检查器中选择你的回调方法
    在脚本文件中创建好你的方法回调,在 属性检查器 中选择你的回调函数。
    cc.Class({ extends: cc.Component, properties: { }, callback(toggle, customEventData) { alert("Toggle1 " + customEventData); }, start () { },});

    2.2 ToggleContainer直接新建UI节点->ToggleContainer,我们允许它重复选择,将allowSwitchOff勾选。
    3. 总结事实上,toggle事件添加有很多方式

    方法一 纯代码添加回调方法二 通过 toggle.node.on(‘toggle’, …) 的方式来添加方法三 用代码获取UI控件方法四 直接在属性检查器中选择你的回调方法
    对于现在写好了的回调函数已经够用了,所以一般使用最后一种方式。
    本教程部分素材来源于网络。
    1 回答 2018-11-29 19:59:39
  • 编程实现监控U盘或者其它移动设备的插入和拔出

    背景如果在没有阅读本文之前,可能你会认为编程实现监控U盘或者其它移动设备的插入和拔出,是一个很难的事情,或者是一个很靠近系统底层的事情。其实,这些你完全不用担心,Windows 已经为我们都设计好了。
    我们都知道,Windows应用程序都是消息(事件)驱动的,任何一个窗口都能够接收消息,并对该消息做出相应的处理。同样,U盘或者其它移动设备的插入或者拔出也会有相应的消息与之对应,这个消息便是 WM_DEVICECHANGE。顾名思义,这个消息就是设备更改的时候产生的。那么,我们的程序同样可以捕获到这个消息,只要我们对这个消息做出处理就可以了。
    现在,我就把这个程序实现的过程整理成文档,分享给大家。
    函数介绍WM_DEVICECHANGE 消息
    通知应用程序对设备或计算机的硬件配置进行更改。窗口通过其WindowProc函数接收此消息。
    LRESULT CALLBACK WindowProc( HWND hwnd, // handle to window UINT uMsg, // WM_DEVICECHANGE WPARAM wParam, // device-change event LPARAM lParam // event-specific data );
    参数

    hwnd:窗口的句柄。
    uMsg:WM_DEVICECHANGE标识符。
    wParam:发生的事件。 该参数可以是Dbt.h头文件中的以下值之一:




    VALUE
    MEANING




    DBT_CONFIGCHANGECANCELED
    更改当前配置(停靠或停靠)的请求已被取消


    DBT_CONFIGCHANGED
    由于停靠或停靠,当前配置已更改


    DBT_CUSTOMEVENT
    发生了自定义事件


    DBT_DEVICEARRIVAL
    已插入设备或介质,现在可以使用


    DBT_DEVICEQUERYREMOVE
    请求删除设备或介质的权限。 任何应用程序可以拒绝此请求并取消删除


    DBT_DEVICEQUERYREMOVEFAILED
    删除设备或介质的请求已被取消


    DBT_DEVICEREMOVECOMPLETE
    已移除设备或介质片


    DBT_DEVICEREMOVEPENDING
    一个设备或一块介质即将被删除。 不能否认


    DBT_DEVICETYPESPECIFIC
    发生设备特定事件


    DBT_DEVNODES_CHANGED
    已将设备添加到系统中或从系统中删除


    DBT_QUERYCHANGECONFIG
    请求权限更改当前配置(停靠或停靠)


    DBT_USERDEFINED
    此消息的含义是用户定义的




    lParam:指向包含事件特定数据的结构的指针。 其格式取决于wParam参数的值。 有关详细信息,请参阅每个事件的文档。
    返回值

    返回TRUE以授予请求。返回BROADCAST_QUERY_DENY以拒绝该请求。

    DEV_BROADCAST_HDR 结构体
    typedef struct _DEV_BROADCAST_HDR { DWORD dbch_size; DWORD dbch_devicetype; DWORD dbch_reserved;} DEV_BROADCAST_HDR, *PDEV_BROADCAST_HDR;
    成员

    dbch_size这个结构的大小,以字节为单位。如果这是用户定义的事件,则该成员必须是此标头的大小,加上_DEV_BROADCAST_USERDEFINED结构中的可变长度数据的大小。
    dbch_devicetype设备类型,用于确定前三个成员之后的事件特定信息。 该成员可以是以下值之一:




    VALUE
    MEANING




    DBT_DEVTYP_DEVICEINTERFACE
    设备类。 此结构是DEV_BROADCAST_DEVICEINTERFACE结构


    DBT_DEVTYP_HANDLE
    文件系统句柄。 这个结构是一个DEV_BROADCAST_HANDLE结构


    DBT_DEVTYP_OEM
    OEM或IHV定义的设备类型。 该结构是DEV_BROADCAST_OEM结构


    DBT_DEVTYP_PORT
    端口设备(串行或并行)。 这个结构是一个DEV_BROADCAST_PORT结构


    DBT_DEVTYP_VOLUME
    逻辑卷。 这个结构是一个DEV_BROADCAST_VOLUME结构




    dbch_reserved
    保留。


    DEV_BROADCAST_VOLUME 结构体
    typedef struct _DEV_BROADCAST_VOLUME { DWORD dbcv_size; DWORD dbcv_devicetype; DWORD dbcv_reserved; DWORD dbcv_unitmask; WORD dbcv_flags;} DEV_BROADCAST_VOLUME, *PDEV_BROADCAST_VOLUME;
    成员

    dbcv_size这个结构的大小,以字节为单位。
    dbcv_devicetype设置为DBT_DEVTYP_VOLUME(2)。
    dbcv_reserved保留; 不使用。
    dbcv_unitmask标识一个或多个逻辑单元的逻辑单元掩码。 掩码中的每个位对应于一个逻辑驱动器。 位0表示驱动器A,位1表示驱动器B,依此类推。
    dbcv_flags此参数可以是以下值之一:




    VALUE
    MEANING




    DBTF_MEDIA
    更改影响驱动器中的介质。 如果未设置,更改将影响物理设备或驱动器


    DBTF_NET
    指示逻辑卷是一个网络卷




    实现原理由于我们主要是对设备的插入和拔出做操作,所以,只需要对消息回调函数的参数 wParam 进行判断,是否为设备已插入操作 DBT_DEVICEARRIVAL 和 设备已移除操作 DBT_DEVICEREMOVECOMPLETE。然后再重点分析相应操作对应的 lParam 参数里存储的信息数据,从而分析出产生操作设备的盘符。
    设备已插入 DBT_DEVICEARRIVAL首先,当 wParam 为设备已插入操作 DBT_DEVICEARRIVAL的时候,我们就可以知道是有设备已经插入了。
    接下来就是要获取设备的盘符,这需要对参数 lParam 进行分析,此时 lParam 则表示指向 DEV_BROADCAST_HDR 结构的指针。由上述的结构体介绍中,我们可以知道,要获取盘符,就首先要判断 DEV_BROADCAST_HDR 结构体中的设备类型 dbch_devicetype 是否为逻辑卷 DBT_DEVTYP_VOLUME。因为其它的消息类型,是不会产生盘符的。只有消息类型为 DBT_DEVTYP_VOLUME 逻辑卷,才会产生盘符。
    由上述结构体介绍中知道,当消息类型为 DBT_DEVTYP_VOLUME 逻辑卷的时候, 参数 lParam 实际上是结构体 DEV_BROADCAST_VOLUME。其中,结构体 DEV_BROADCAST_VOLUME 的 dbcv_unitmask 成员标识一个或多个逻辑单元的逻辑单元掩码,掩码中的每个位对应于一个逻辑驱动器。 位0表示驱动器A,位1表示驱动器B,依此类推。所以,我们可以根据 dbcv_unitmask 计算出设备生成的盘符。
    设备已移除 DBT_DEVICEREMOVECOMPLETE首先,当 wParam 为设备已移除操作 DBT_DEVICEREMOVECOMPLETE 的时候,我们就可以知道是有设备已经移除了。
    接下来就是要获取移除设备原来的盘符,这需要对参数 lParam 进行分析,此时 lParam 则表示指向 DEV_BROADCAST_HDR 结构的指针。接下来的分析,和上面设备插入时,获取设备盘符的分析是一样的,在此就不重复了。
    编程实现给程序添加 WM_DEVICECHANGE 的消息响应,并声明定义一个处理函数,处理相应的消息。
    对于 Windows应用程序 来说,只需要在窗口消息处理函数中,增加消息类型WM_DEVICECHANGE 的判断即可。然后,调用处理函数进程处理。
    对于 MFC 程序来说,则需要自定义 WM_DEVICECHANGE 消息响应函数。在主对话框类的头文件中声明处理函数 :
    LRESULT OnDeviceChange(WPARAM wParam, LPARAM lParam);
    然后,再在主对话框类的消息映射列表中,添加 WM_DEVICECHANGE 与消息处理函数的映射:
    BEGIN_MESSAGE_MAP(CWM_DEVICECHANGE_MFC_TestDlg, CDialogEx) … …(省略) ON_MESSAGE(WM_DEVICECHANGE, OnDeviceChange) … …(省略)END_MESSAGE_MAP()
    那么,Windows应用程序 和 MFC 程序对 WM_DEVICECHANGE 消息的消息处理函数定义都是相同的:
    LRESULT OnDeviceChange(WPARAM wParam, LPARAM lParam){ switch (wParam) { // 设备已经插入 case DBT_DEVICEARRIVAL: { PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; // 逻辑卷 if (DBT_DEVTYP_VOLUME == lpdb->dbch_devicetype) { // 根据 dbcv_unitmask 计算出设备盘符 PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)lpdb; DWORD dwDriverMask = lpdbv->dbcv_unitmask; DWORD dwTemp = 1; char szDriver[4] = "A:\\"; for (szDriver[0] = 'A'; szDriver[0] <= 'Z'; szDriver[0]++) { if (0 < (dwTemp & dwDriverMask)) { // 获取设备盘符 ::MessageBox(NULL, szDriver, "设备已插入", MB_OK); } // 左移1位, 接着判断下一个盘符 dwTemp = (dwTemp << 1); } } break; } // 设备已经移除 case DBT_DEVICEREMOVECOMPLETE: { PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; // 逻辑卷 if (DBT_DEVTYP_VOLUME == lpdb->dbch_devicetype) { // 根据 dbcv_unitmask 计算出设备盘符 PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)lpdb; DWORD dwDriverMask = lpdbv->dbcv_unitmask; DWORD dwTemp = 1; char szDriver[4] = "A:\\"; for (szDriver[0] = 'A'; szDriver[0] <= 'Z'; szDriver[0]++) { if (0 < (dwTemp & dwDriverMask)) { // 获取设备盘符 ::MessageBox(NULL, szDriver, "设备已移除", MB_OK); } // 左移1位, 接着判断下一个盘符 dwTemp = (dwTemp << 1); } } break; } default: break; } return 0;}
    程序测试这是,我们直接运行程序,插入U盘,程序成功弹窗提示有U盘插入,并给出U盘的盘符。

    我们把U盘拔出,程序成功弹窗提示有U盘拔出,并给出U盘的盘符。

    总结本文给出了 MFC程序 和 Windows应用程序 的例子,实现监控U盘或其它移动设备的插入和拔出。其中,我们需要注意理解 DEV_BROADCAST_VOLUME 的 dbcv_unitmask 逻辑单元掩码。它是 4字节 32位,它的每一位都对应一个盘符,从 A 开始计数。如果位中的数值为1,则表示设备操作产生的盘符,为 0,则表示没有产生盘符。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-11-28 20:55:37
  • 【Cocos Creator实战教程(8)】——UI组件(2)ProgressBar 组件

    1. 知识点讲解ProgressBar(进度条)经常被用于在游戏中显示某个操作的进度,在节点上添加 ProgressBar 组件,然后给该组件关联一个 Bar Sprite 就可以在场景中控制 Bar Sprite 来显示进度了。

    点击 属性检查器 下面的添加组件按钮,然后从添加 UI 组件中选择ProgressBar,即可添加 ProgressBar 组件到节点上。
    1.1 ProgressBar 属性


    属性
    功能说明




    Bar Sprite
    进度条渲染所需要的 Sprite 组件,可以通过拖拽一个带有 Sprite组件的节点到该属性上来建立关联。


    Mode
    支持 HORIZONTAL(水平)、VERTICAL(垂直)和 FILLED(填充)三种模式,可以通过配合 reverse 属性来改变起始方向。


    Total Length
    当进度条为 100%时 Bar Sprite 的总长度/总宽度。在 FILLED 模式下 Total Length 表示取 Bar Sprite 总显示范围的百分比,取值范围从 0 ~ 1。


    Progress
    浮点,取值范围是 0~1,不允许输入之外的数值。


    Reverse
    布尔值,默认的填充方向是从左至右/从下到上,开启后变成从右到左/从上到下。



    1.2 详细说明添加 ProgressBar 组件之后,通过从 层级管理器 中拖拽一个带有Sprite组件的节点到 Bar Sprite属性上,此时便可以通过拖动 progress 滑块来控制进度条的显示了。
    Bar Sprite 可以是自身节点,子节点,或者任何一个带有Sprite组件的节点。另外,Bar Sprite 可以自由选择 Simple、Sliced 和 Filled 渲染模式。
    进度条的模式选择 FILLED 的情况下,Bar Sprite 的 Type 也需要设置为 FILLED,否则会报警告。
    2. 步骤创建新的ProgressScene,创建ProgressBar控件名为progressBarView,然后在progressBarView上添加脚本文件ProgressBarScript。
    创建脚本文件,当然我们在创建脚本文件的时候,需要自定义properties属性来让脚本这个类来接收UI控件并关联
    cc.Class({ extends: cc.Component, // 脚本自定义的属性,当前自定义的属性会在属性检查中查看到 properties: { speed: 1, progressBarView: { type: cc.ProgressBar, default: null } }, //当我们将脚本添加到节点 `node`上面的时候 onLoad: function () { this._ping = true; this.progressBarView.progress = 0; }, //如果该组件启用,则每帧调用 update //dt:Number the delta time in seconds it took to complete the last frame update: function (dt) { this._updateProgressBar(this.progressBarView, dt); }, _updateProgressBar: function(progressBar, dt){ var progress = progressBar.progress; if(progress < 1.0 && this._ping){ progress += dt * this.speed; } else { progress -= dt * this.speed; this._ping = progress <= 0; } progressBar.progress = progress; }});
    当我们在写完这些的时候,将脚本文件添加到节点上面,拖拽创建的控件ProgressBarView到我的属性progressBarView上,这样程序就会发现进度条在走。

    3. 总结在现实中,我们可能需要从服务器预加载资源,这时就需要进度条来表示进度。比如使用cc.loader加载资源,同时监控加载进度,之后再用update显示。
    本资源部分素材来源于网络。
    1 回答 2018-11-28 14:54:25
  • 基于WinPcap实现的UDP发包程序

    背景一天,一位同学打电话给我说,让我帮忙开发一个基于WinPcap工具的UDP发包工具,还特地叮嘱是基于WinPcap,不要原始套接字Raw Socket。而且,时间只有一个白天,它晚上就要,而打电话给我的时候,已经临近中午了。我一听,同学一场,那就举手之劳吧。
    之前,自己就是用WinPcap开发过一些小程序,例如网卡遍历程序、Arp欺骗攻击程序等,所以,对WinPcap还算是熟悉。做这样的UDP发包程序,应该倒也不是那没事。
    现在,为了配合这篇文章的讲解,我特地对这个程序重新开发。把程序的实现原理和过程整理成文档,分享给大家。
    使用VS2013配置WinPcap开发环境程序是使用VS2013开发的,程序中要使用WinPcap工具提供的功能函数的话,就需要将对VS2013进行配置,将WinPcap库导入到程序中,现在,就先介绍WinPcap环境的配置过程:
    1.下载并安装WinPcap运行库http://www.winpcap.org/install/default.htm 。一些捕包软件会捆绑安装WinPcap,MentoHust也会附带WinPcap,这种情况下一般可以跳过此步。
    2.下载WinPcap开发包http://www.winpcap.org/devel.htm ,解压到纯英文路径。
    3.打开VS工程项目,在VS工程项目的 属性 —> VC++目录 中,包含目录 选项添加WpdPack\Include 目录,在 库目录 选项中添加 WpdPack\Lib 目录。
    4.在 属性 —> C/C++ —> 预处理器 中,添加 WPCAP 和 HAVE_REMOTE 这两个宏定义。
    5.在VS工程项目的 属性 —> 链接器 —> 输入 中,添加 wpcap.lib 和 ws2_32.lib 两个库。
    这样,就可以将WinPcap所需的库文件包含到工程项目中了。接下来,我们就开始讲解UDP发包程序的实现过程。
    实现过程1. 获取网卡设备列表首先,我们调用WinPcap函数pcap_findalldevs_ex获取设备列表信息,函数会将计算机上所有网卡设备信息返回到指向pcap_if_t结构体指针里。我们只需要对这个结构体指针进行遍历,就可以获取每个设备的详细信息。
    // 获取网卡设备列表 if (-1 == pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, szErr)) { ShowError("pcap_findalldevs_ex"); return; }
    程序将每个设备的信息显示在界面上,供用户选中使用哪个网卡进行操作。
    2. 设置网卡信息并打开网卡我们使用 pcap_open 函数来设置并打开网卡,函数中第 1 个参数stAdapterInfo. szAdapterName,表示网卡的名称;第 2 个参数 65535,表示设置保留数据包的长度,65535即每个数据包的前65535字节长度的数据被保留在缓冲区中;第 3 个参数 PCAP_OPENFLAG_DATATX_UDP 表示使用UDP协议来处理数据传输;第 4 个参数 1 ,表示以毫秒为单位的读取超时时间。读取超时用来处理,捕获一个数据包后,读操作并不必需要立即返回的情况。但这可能等待一些时间以允许捕获更多的数据包,这样用户层的一次读操作就可从操作系统的内核中读取更多的数据包。第 5 个参数为 NULL;第 6 个参数 errbuf,可以获取返回的出错信息。
    // 打开网卡 m_adhandle = pcap_open(stAdapterInfo.szAdapterName, 65535, PCAP_OPENFLAG_DATATX_UDP, 1, NULL, errbuf); if (NULL == m_adhandle) { ShowError("pcap_open"); return; }
    3. 构造UDP数据包我们根据源MAC地址、目的MAC地址、源IP地址、目的IP地址、源端口、目的端口、数据内容以及数据内容长度,构造UDP数据包。具体构造过程如下:

    首先,构造以太网帧头。
    memcpy((void*)FinalPacket, (void*)DestinationMAC, 6); memcpy((void*)(FinalPacket + 6), (void*)SourceMAC, 6); USHORT TmpType = 8; memcpy((void*)(FinalPacket + 12), (void*)&TmpType, 2);
    然后,构造IP头。
    memcpy((void*)(FinalPacket + 14), (void*)"\x45", 1); memcpy((void*)(FinalPacket + 15), (void*)"\x00", 1); TmpType = htons(TotalLen); memcpy((void*)(FinalPacket + 16), (void*)&TmpType, 2); TmpType = htons(0x1337); memcpy((void*)(FinalPacket + 18), (void*)&TmpType, 2); memcpy((void*)(FinalPacket + 20), (void*)"\x00", 1); memcpy((void*)(FinalPacket + 21), (void*)"\x00", 1); memcpy((void*)(FinalPacket + 22), (void*)"\x80", 1); memcpy((void*)(FinalPacket + 23), (void*)"\x11", 1); memcpy((void*)(FinalPacket + 24), (void*)"\x00\x00", 2); memcpy((void*)(FinalPacket + 26), (void*)&SourceIP, 4); memcpy((void*)(FinalPacket + 30), (void*)&DestIP, 4);
    接着,构造UDP头。
    TmpType = htons(SourcePort); memcpy((void*)(FinalPacket + 34), (void*)&TmpType, 2); TmpType = htons(DestinationPort); memcpy((void*)(FinalPacket + 36), (void*)&TmpType, 2); USHORT UDPTotalLen = htons(UserDataLen + 8); memcpy((void*)(FinalPacket + 38), (void*)&UDPTotalLen, 2); memcpy((void*)(FinalPacket + 42), (void*)UserData, UserDataLen);

    要注意的是,IP校验和以及UDP的校验和计算。这样,我们就可以成功构造UDP的数据包,接下来,就可以对数据包进行发送。
    4. 发送UDP数据包我们使用 pcap_sendpacket 函数发送单个数据包,第 1 个参数表示打开网卡的时候获取的句柄;第 2 个参数就是发送的数据内容;第 3 个参数表示发送数据内容的长度。注意,缓冲数据将直接发送到网络,而不会进行任何加工和处理。这就意味着应用程序需要创建一个正确的协议首部,来使这个数据包更有意义。
    // 发送数据包 if (0 != pcap_sendpacket(m_adhandle, FinalPacket, (UserDataLen + 42))) { char *szErr = pcap_geterr(m_adhandle); ShowError(szErr); return; }
    经过上面 4 步操作,便可以实现使用 WinPcap 发送 UDP 数据包了。要注意IP校验和以及UDP校验和的计算,这两个的值注意不要算错。
    程序测试我们以管理员权限运行程序,并对一个UDP程序发包,观察UDP程序能否接收到我们发包程序发送的UDP数据包。其中,UDP程序使用的是《Socket通信之UDP通信小程序》这篇文章中实现的UDP程序。
    经过测试结果,UDP程序成功接收到UDP数据包。

    总结这个程序是基于WinPcap实现的,所以,可能有很多人之前还没有接触过WinPcap方面的知识,所以,一下子使用和理解起来就比较困难。关键是耐下心来,把程序中调用到的不理解的WinPcap函数,在网上查找说明,对函数理解清晰。
    其中,要注意的是,使用WinPcap工具,需要有管理员权限。
    参考参考自《Windows黑客编程技术详解》一书
    3 回答 2018-11-28 11:22:48
  • 【Cocos Creator实战教程(7)】——UI组件(1)ScrollView 组件

    1. 知识点讲解ScrollView 是一种带滚动功能的容器,它提供一种方式可以在有限的显示区域内浏览更多的内容。通常 ScrollView 会与Mask组件配合使用,同时也可以添加ScrollBar组件来显示浏览内容的位置。
    1.1 ScrollView 属性


    属性
    功能说明




    content
    它是一个节点引用,用来创建 ScrollView 的可滚动内容,通常这可能是一个包含一张巨大图片的节点。


    Horizontal
    布尔值,是否允许横向滚动。


    Vertical
    布尔值,是否允许纵向滚动。


    Inertia
    滚动的时候是否有加速度。


    Brake
    浮点数,滚动之后的减速系数。取值范围是 0-1,如果是 1 则立马停止滚动,如果是 0,则会一直滚动到 content 的边界。


    Elastic
    布尔值,是否回弹。


    Bounce Duration
    浮点数,回弹所需要的时间。取值范围是 0-10。


    Horizontal ScrollBar
    它是一个节点引用,用来创建一个滚动条来显示 content 在水平方向上的位置。


    Vertical ScrollBar
    它是一个节点引用,用来创建一个滚动条来显示 content 在垂直方向上的位置


    ScrollView Events
    列表类型,默认为空,用户添加的每一个事件由节点引用,组件名称和一个响应函数组成。详情见 ‘Scrollview 事件’ 章节


    CancelInnerEvents
    如果这个属性被设置为 true,那么滚动行为会取消子节点上注册的触摸事件,默认被设置为 true。



    1.2 ScrollView 事件


    属性
    功能说明




    Target
    带有脚本组件的节点。


    Component
    脚本组件名称。


    Handler
    指定一个回调函数,当 ScrollView 的事件发生的时候会调用此函数。


    CustomEventData
    用户指定任意的字符串作为事件回调的最后一个参数传入。



    Scrollview 的事件回调有两个参数,第一个参数是 ScrollView 本身,第二个参数是 ScrollView 的事件类型。
    1.3 详细说明ScrollView组件必须有指定的content节点才能起作用,通过指定滚动方向和content节点再此方向长度来计算滚动时的位置信息,content节点也可以通过UIwidget设置自动resize。
    通常的一个ScrollView的节点树如下:

    这里的View用来定义一个可以显示的滚动区域,所以通常Mask组件会被添加到View上面,滚动的内容可以直接放到content节点或者添加到content中的子节点上。
    1.4 ScrollBar 设置ScrollBar 是可选的,你可以选择只设置水平或者垂直 ScrollBar,当然也可以两者都设置。建立关联可以通过在 层级管理器 里面拖拽一个带有 ScrollBar 组件的节点到 ScrollView 的相应字段完成。
    1.5 通过脚本代码添加回调1.5.1 方法一这种方法添加的事件回调和使用编辑器添加的事件回调是一样的,通过代码添加, 你需要首先构造一个 cc.Component.EventHandler 对象,然后设置好对应的 target, component, handler 和 customEventData 参数。
    cc.Class({ extends: cc.Component, properties: { }, onLoad () { var scrollViewEventHandler = new cc.Component.EventHandler(); scrollViewEventHandler.target = this.node; //这个 node 节点是你的事件处理代码组件所属的节点 scrollViewEventHandler.component = "ScrollView1Script";//这个是脚本文件名 scrollViewEventHandler.handler = "callback"; scrollViewEventHandler.customEventData = "foobar"; var scrollview = this.node.getComponent(cc.ScrollView); scrollview.scrollEvents.push(scrollViewEventHandler); }, //注意参数的顺序和类型是固定的 callback: function (scrollview, eventType, customEventData) { //这里 scrollview 是一个 Scrollview 组件对象实例 //这里的 eventType === cc.ScrollView.EventType enum 里面的值 //这里的 customEventData 参数就等于你之前设置的 "foobar" console.log(customEventData); }});
    1.5.2 方法二通过 scrollview.node.on(‘scroll-to-top’, …) 的方式来添加
    //假设我们在一个组件的 onLoad 方法里面添加事件处理回调,在 callback 函数中进行事件处理:cc.Class({ extends: cc.Component, properties: { scrollview: cc.ScrollView }, onLoad: function () { this.scrollview.node.on('scroll-to-top', this.callback, this); }, callback: function (event) { //这里的 event 就是这个ScrollView 组件 //另外,注意这种方式注册的事件,也无法传递 customEventData }});
    2. 步骤2.1 建立节点树操作
    在层级管理器中添加一个新的UI组件ScrollView名为NewScrollView,节点树如下
    将content其中的item删除,也可以直接在item上修改,我是直接删除,添加一个Scrite(精灵),将下载的图片拖到资源管理器,然后在将图片拖入到Scrite(精灵)中的Scrite Frame中(原谅我放上了我最爱的偶吧——允浩)
    复制一份ScrollBar分别命名为scrollBarV、scrollBarH,横向滚动条、竖向滚动条
    调整New ScrollView子节点的位置

    节点树完成图如下

    2.2 创建Widget对齐组件、自动布局
    在New ScrollView节点上添加widget对齐挂件,设置全屏幕


    View节点设置全屏幕



    属性





    left
    0 px


    right
    0 px


    top
    0 px


    bottom
    0 px




    content节点的widget以左上角对齐,并添加Layout组件设置Resize Mode



    属性





    left
    0 px


    top
    0 px



    Layout组件中设置



    属性

    说明




    Resize Mode
    CONTAINER
    对容器的大小进行缩放,就是content根据子节点的大小来自动调整大小全部显示出子节点



    2.3 设置content中节点树中的的属性检查器
    设置ScrollBarV的属性,如图


    设置ScrollBarH的属性,如图

    2.4 设置New ScrollView的属性检查器
    3. 总结这里我们的游戏的菜单运用了这种方法,滑动可以玩两个游戏。请试试如何创建吧。
    提示:

    想想每个节点的大小都是多少,有做出来的可以在评论区写出来哦。
    本资源部分素材来源于网络。
    1 回答 2018-11-27 19:30:17
  • 【Cocos Creator实战教程(5)】——打砖块(物理引擎,碰撞检测)

    1. 知识点
    物理引擎碰撞检测
    2. 步骤2.1 准备工作搭一个游戏背景
    2.2 小球运动再建一个物理层,用来装游戏里的带有物理属性的东西,设置锚点为左下角

    wall:墙//小球碰到就会反弹的那种墙 ground:地面//球碰到地面,这局游戏就结束了 brick_layout:砖块布局//这个单词我们之前讲过了就不讲了 ball:球//就是球 paddle:桨//这里特指那个可以控制移动的白色长方形

    这个wall肯定是要有碰撞属性的,在属性面板,添加一个物理组件 (物理->rigidbody)。
    因为我们的墙有上,左,右三面,所以再添加三个碰撞组件(一个节点可以有多个碰撞组件)。

    编辑一下

    地面同理,小球同理,托盘同理 。(这里把地面和墙分开是为了后面墙和地面可能有不同的逻辑)
    现在已经编辑了几个物理节点的碰撞包围盒,但还没有编辑他们的物理属性(cc.RigidBody)
    先从小球开始,点击ball节点,在属性检查器可以看到

    Cocos Creator从1.5版本开始支持Box2D物理游戏引擎,Box2D是一个优秀的刚体模拟框架,关于Box2D的知识可以去网络上自行了解。
    把第一个参数勾选,代表启用碰撞回调,可以在脚本里写回调函数
    Bullet:高速运动的物体(子弹)开启,避免穿透,这里不用勾选
    type选择Dynamic,

    static:不会受到力的影响,不会受到速度影响,指的是物理引擎,我们依然可以通过移动节点来改变位置 。
    kinematic:不受力的影响,会受到速度影响 。
    dynamic:受力影响,受速度影响 。
    animated:和动画结合使用。

    Gravity Scale设置为0(标准是1,数值代表比例),也就是没有重力。
    设置线速度(1000,1000)
    在下面的碰撞组件里,设置Friction (摩擦系数)等于0(没有摩擦力),Restitution(弹性系数)等于1(没有动量损耗)

    因为小球是我们的主角,左右的碰撞都是对球来说的,所以碰撞属性都在小球这一方设置就可以了。
    另外要设置wall,ground,paddle,brick的type为staticbrick的tag为1,ground的tag为2,paddle的tag为3,wall的tag位4
    下面来看脚本
    BrickLayout.js
    cc.Class({ extends: cc.Component, properties: { padding: 0, spacing: 0, cols: 0, brickPrefab: cc.Prefab, bricksNumber: 0, }, init(bricksNumber) { this.node.removeAllChildren(); this.bricksNumber = bricksNumber; for (let i = 0; i < this.bricksNumber; i++) { let brickNode = cc.instantiate(this.brickPrefab); brickNode.parent = this.node; brickNode.x = this.padding + (i % this.cols) * (brickNode.width + this.spacing) + brickNode.width / 2; brickNode.y = -this.padding - Math.floor(i / this.cols) * (brickNode.height + this.spacing) - brickNode.height / 2; } }});
    2.3 添加砖块自己写了一个动态添加砖块的布局脚本,传入需要添加的砖块数量就可以动态加入的布局节点中。
    2.4 结束界面完善好游戏逻辑,我使用了MVC模式编写脚本。

    3. 总结至此,我便给大家简要的介绍了一下物理引擎,更多的功能需要大家自己探索实践。
    列出几个大家常问的问题:
    3.1如何移动刚体?当我们的一个节点上有一个刚体,我们要进行移动。一般我们都会通过节点的setPosition进行移动,但是刚体不会被影响,不管是Static、还是Dynamic还是Kinematic都不会被影响我们可以通过1、瞬时动作cc.place来进行移动而且不会影响刚体原本的运动轨迹2、Action的所有动作。cc.moveBy;cc.moveTo;等等
    3.2 碰撞组件和物理组件有什么不同?碰撞组件没有类型之分,只要相交就会发生碰撞事件,如果不对碰撞进行处理,那就没有任何影响。物理碰撞组件分类型,因为他们先会绑定刚体。如果刚体类型不同则会有不同的效果。和Dynamtic类型刚体绑定的PhysicsBoxCollider会受重力影响,可以设置速度和Static类型刚体绑定的物理组件,不会受重力影响,不可以设置速度,可以通过设置位置让其移动和Kinematic类型刚体绑定的物理组件,不受重力影响,可以设置速度
    例如,本文中我们就是用了物理碰撞组件,所以刚体类型选择上要有一定技巧。
    在现实开发情况下,拾取道具和横像动作例如进攻多用碰撞组件 ,而竖向动作例如弹跳多用物理碰撞组件。
    3.3 三种物理组件有什么不同?绑定了Dynamic(运动)类型的物理组件不能穿透绑定了Static(静态)类型的物理组件绑定了Dynamic类型的物理组件不能穿透绑定了Kinematic类型的物理组件Static和Kinematic不会触发碰撞事件,Static和Static;Kinematic和Kinematic不会触发碰撞事件;
    所以,因为我们不能将ball选为kinematic,尽管此游戏我们忽略了重力。
    3.4 物理组件如何进行碰撞回调?首先RigidBody要开启碰撞监听然后当前节点下有如下函数
    在函数碰撞体刚开始接触时调用一次onBeginContatct:function(contact,selfCollider,otherCollider){}在两个碰撞体结束接触时被调用一次onEndContact:fucntion(contact,setCollider,otherCollider){}每次要处理碰撞体接触逻辑是被调用onPreSolve:function(contact,selfCollider,otherCollider){}每次处理完碰撞体接触时被调用onPostSolve:fucntion(contact,selfCollider,otherCollider){}
    3.5 碰撞组件的回调var manager = cc.director.getCollisionManager();manager.enabled = true;脚本里面先开启碰撞监听,因为默认是关闭然后有以下函数://当碰撞产生时调用onCollisionEnter:function(other,self){}//在碰撞产生后,在碰撞结束前,每次计算完碰撞结果后调用onCollisionStay:function(other,self){}//当碰撞结束后调用onCollisionExit;function(other,self){}
    部分素材来源于网络,欢迎提问
    4 回答 2018-11-25 22:52:54
  • 【Cocos Creator实战教程(4)】——炸弹人(TiledMap相关)

    1. 相关知识点
    虚拟手柄(新)
    地图制作
    碰撞检测
    动画制作

    2. 步骤2.1 制作地图2.1.1.新建19x19的地图,Tile大小32x32,导入图块资源2.1.2 建立三个图层(ground,hide,main)和一个对象层(objects)ground是背景层,用绿色的草坪图块填充满

    hide是道具层,挑选道具图块(包括出口的门)放在几个位置

    main是障碍物层,还原经典炸弹人的地图布局,每隔一个位置放一个钢块,用土块覆盖道具层的道具,在其他位置再放几个土块

    objects层有两个对象(player,enemy)标记玩家和敌人的位置信息
    最终效果如图

    2.1.3 将除了草坪图块的其他图块添加type属性,属性值分别为,soil(土块),steel(钢块),door(门),prop1(道具1)…
    2.1.4 保存地图文件和图集文件放在一起2.2 绑定地图2.2.1 新建工程Bomberman,将所需资源全部导入,项目结构如下(有些是后来添加的)
    2.2.2 将地图拖入场景(自动生成map和layer节点),将锚点改为(0,0),下面出现的player等节点也都是(0,0)为锚点2.2.3 新建脚本game.js添加为map节点组件,声明全局变量,在属性面板中拖入相关层节点
    2.3 虚拟手柄2.3.1 我们准备了上下左右炸弹五个按钮的原始状态和按下状态共十张图片素材,在场景编辑器中看着顺眼的地方摆放这五个按钮(将label删除),并将按钮的相应状态的按钮图片添加到右侧属性面板中2.3.2 为每个按钮添加点击事件,(map->game->btnBomb, btnUp, btnDown, btnLeft, btnRight)
    2.4 炸弹动画2.4.1 将炸弹图片集中的任意一个拖入场景中,将其宽高改为32x32,再拖回资源管理器中制作一个炸弹的prefab bomb,为其添加Animation组件2.4.2 新建炸弹爆炸动画clip explode,拖入bomb的属性面板2.4.3 在动画播放完成的最后一帧加入一个帧事件,响应事件方法名为exploded
    2.5 主角和敌人2.5.1 添加player和enemy为map节点的两个子节点,锚点0,0,大小32x32 (位置随意,一会我们要在代码中动态设置他们的位置)
    3. 总结
    虚拟手柄是一个比较热点的控制,现实生活中还会有圆圈控制各个角度的,也有开源项目大家可以参考
    注意节点前后层的遮挡关系

    注意:本教程部分素材来源于网络,另请大家在评论区踊跃提问发言。
    5 回答 2018-11-24 23:12:26
  • 【Cocos Creator实战教程(3)】——TiledMap(瓦片地图)组件

    1. 前言瓦片地图是由一张一张的正方形小图片拼接成的地图,例如炸弹人,QQ堂都是非常典型的瓦片游戏。瓦片地图(Tile Map) 不但生成简单,并且可以灵活的用于Cocos2d-x引擎。不论你的游戏是角色扮演游戏, 平台动作游戏或仿打砖块游戏,这些游戏地图可以使用开源的瓦片地图编辑器Tiled Map Editor生成并保存为TMX文件格式,被Cocos2d-x支持。
    2. 步骤2.1 安装Tiled Map Editoredit(编辑)->preferences(参数)里面可以设置语言
    2.2 TiledMap制作新建一张地图

    建立三个图层和一个对象层并将资源图块导入 (最下面的图层将显示在最下面)



    ground层:用ground图块填充满(按住鼠标左键)
    barriers层:用barrier图块
    stars层:用star图块

    最终效果如下图

    选择players对象层,在如图位置插入两个图块对象,并更改名称如图

    给星星添加一个属性

    保存为tmx文件,和图片文件放在一起
    2.3 Cocos Creator中使用TiledMap:
    新建一个TiledMapTest工程,创建一个Game场景
    将刚才生成的tmx文件和相关图片一起添加到工程
    将map.tmx文件拖入层级管理器或者属性编辑器,就会自动生成map节点以及其子节点(也就是图层节点)【没有对象层节点】
    最后将player(安卓小机器人)图片拖入(位置随意),创建player节点,并使其为map节点的子节点。
    调整map和player节点的锚点都为(0,0)也就是左下角
    创建map.js脚本添加到map节点

    cc.Class({ extends: cc.Component, properties: { }, // use this for initialization onLoad: function () { this.player = this.node.getChildByName('player'); this.loadMap(); cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); }, onKeyDown:function(event){ var newTile = cc.p(this.playerTile.x, this.playerTile.y); switch(event.keyCode) { case cc.KEY.up: newTile.y -= 1; break; case cc.KEY.down: newTile.y += 1; break; case cc.KEY.left: newTile.x -= 1; break; case cc.KEY.right: newTile.x += 1; break; default: return; } this.tryMoveToNewTile(newTile); }, //加载地图文件时调用 loadMap: function () { //初始化地图位置 this.node.setPosition(cc.visibleRect.bottomLeft); //地图 this.tiledMap = this.node.getComponent(cc.TiledMap); //players对象层 let players = this.tiledMap.getObjectGroup('players'); //startPoint和endPoint对象 let startPoint = players.getObject('startPoint'); let endPoint = players.getObject('endPoint'); //像素坐标 let startPos = cc.p(startPoint.offset.x, startPoint.offset.y); let endPos = cc.p(endPoint.offset.x, endPoint.offset.y); //障碍物图层和星星图层 this.barriers = this.tiledMap.getLayer('barriers'); this.stars = this.tiledMap.getLayer('stars'); //出生Tile和结束Tile this.playerTile = this.startTile = this.getTilePos(startPos); this.endTile = this.getTilePos(endPos); //更新player位置 this.updatePlayerPos(); }, tryMoveToNewTile: function(newTile) { let width = this.tiledMap.node.width; let height = this.tiledMap.node.height; if (newTile.x < 0 || newTile.x >= width) return; if (newTile.y < 0 || newTile.y >= height) return; if (this.barriers.getTileGIDAt(newTile)) {//GID=0,则该Tile为空 cc.log('This way is blocked!'); return false; } this.tryCatchStar(newTile); this.playerTile = newTile; this.updatePlayerPos(); if (cc.pointEqualToPoint(this.playerTile, this.endTile)) { cc.log('succeed'); } }, tryCatchStar: function(newTile){ let GID = this.stars.getTileGIDAt(newTile); let prop = this.tiledMap.getPropertiesForGID(GID); if (this.stars.getTileGIDAt(newTile)) {//GID=0,则该Tile为空 this.stars.removeTileAt(newTile); } }, //将像素坐标转化为瓦片坐标 getTilePos: function(posInPixel) { let mapSize = this.node.getContentSize(); let tileSize = this.tiledMap.getTileSize(); let x = Math.floor(posInPixel.x / tileSize.width); let y = Math.floor(posInPixel.y / tileSize.height); return cc.p(x, y); }, updatePlayerPos: function() { let pos = this.barriers.getPositionAt(this.playerTile); this.player.setPosition(pos); },});
    最终如下图:

    3. 总结#CC.TiledMap:~properties:tmxFile//地图文件mapLoaded//地图加载是调用的函数~function:getMapSize()//setMapSize()//getTileSize()//setTileSize()//getLayer(name)//returns TieldLayergetObjectGroup(name)//returns TMXObjectGroupgetPropertiesForGID(GID)//returns Object(属性字典)#CC.TieldLayer getPositionAt(pos)//returns Vec2(像素坐标) 参数是瓦片坐标removeTileAt(pos)//瓦片坐标getTileGIDAt(pos)//returns Number(全局唯一标识,0为空)getTileAt(pos)//returns _ccsg.Sprite //removeChild(sprite);setTileGID(gid,pos)//相当于在pos位置添加GID的图块(原来的图块删除)getTileSize()//setTleSize()//getMapTileSize()#TMXObjectGroup:~properties:~function:var getObject(var objectName)//返回属性字典#_ccsg.Sprite://cocos js 里的Sprite,继承自CC.Node,而不是组件~properties:xywidthheightopacity...//节点的属性都有~function:var setSpriteFrame(var spriteFrameName)var runAction(var action) ...//节点的方法都有

    图块放置的位置是基于像素坐标,而图块在map中的索引是基于瓦片坐标(整数),所以在脚本文件中要适时转变
    对象层的对象(比如我们上面做的两个player point),在Cocos Creator中是不会显示的
    对象层记录的是位置信息,图层记录的是图片信息
    .tmx文件其实保存的图块的一种映射关系,所以图块文件和地图文件要始终放在一起,不然可能会显示出错
    GID是图块在一个地图文件中的唯一ID,(图块指的是右下角的图块文件,每一个图块都不相同,瓦片指的指地图中的块,可以是相同的图块),GID为0说明该Tile的图块为空
    当利用getPropertiesForGID(GID)之类的方法时,得到的返回值是属性字典,可以直接通过点方法得到其属性值,属性值都是字符串类型
    当用到getTileAt(pos)时,得到的返回值是Cocos2d 的Sprite,而不是Cocos Creator的Sprite,相关方法可以查找Cocos2d js的API,需要注意的一点是,Cocos2d的Sprite是继承自节点的,而不是组件式,所以我们可以直接这样写“mySprite.x = 100”而不是“mySprite.node.x = 100”
    地图中同一层只能使用一张图集里的图块

    注意:本教程资源部分来源于网络
    1 回答 2018-11-24 18:06:17
  • 创建系统服务实现开机自启动

    背景想必实现程序开机自启动,是很常见的功能了。无论是恶意程序,还是正常的应用软件,都会提供这个功能,方便用户的使用。程序开机自启动,顾名思义,就是计算机开机后,不用人为地去运行程序,程序就可以自己运行起来。对于这个功能的,一直都是杀软重点监测的地方。因为,对于病毒来说,重要的不是如何被破坏,而是如何启动。
    在过去写的大大小小的程序中,我也实现过程序自启动的功能。现在,我把这些自启动功能的实现方式进行下总结。常见的方式有:修改开机自启动注册表、开机自启动目录、创建开机自启计划任务、创建开机自启系统服务等方式。现在对这些技术一一进行分析,并形成文档分享给大家。本文介绍创建系统服务实现开机自启动的方式,其它的实现方式可以搜索我写的相关系列的文档。
    函数介绍OpenSCManager 函数
    建立了一个到服务控制管理器的连接,并打开指定的数据库。
    函数声明
    SC_HANDLE OpenSCManager( LPCTSTR lpMachineName, // computer name LPCTSTR lpDatabaseName, // SCM database name DWORD dwDesiredAccess // access type);
    参数

    lpMachineName指向零终止字符串,指定目标计算机的名称。如果该指针为NULL ,或者它指向一个空字符串,那么该函数连接到本地计算机上的服务控制管理器。
    lpDatabaseName指向零终止字符串,指定将要打开的服务控制管理数据库的名称。此字符串应被置为 SERVICES_ACTIVE_DATABASE。如果该指针为NULL ,则打开默认的 SERVICES_ACTIVE_DATABASE数据库。
    dwDesiredAccess指定服务访问控制管理器的权限。在授予要求的权限前,系统会检查调用进程的权限令牌,该令牌针对与服务控制管理器相关的安全描述符的权限控制列表。此外,该函数的调用将隐式地指定SC_MANAGER_CONNECT 的访问权限。此外,下列服务控制管理器对象的访问类型可以被指定:




    VALUE
    MEANING




    SC_MANAGER_ALL_ACCESS
    除了所有此表中列出的访问类型,还包括STANDARD_RIGHTS_REQUIRED


    SC_MANAGER_CONNECT
    可以连接到服务控制管理器


    SC_MANAGER_CREATE_SERVICE
    使要求的CreateService函数创建一个服务对象,并将其添加到数


    SC_MANAGER_ENUMERATE_SERVICE
    使要求的EnumServicesStatus功能清单的服务,这是在数据库


    SC_MANAGER_LOCK
    使要求的LockServiceDatabase功能获得锁定数据库


    SC_MANAGER_QUERY_LOCK_STATUS
    使要求的QueryServiceLockStatus检索功能锁定状态信息的数据



    返回值

    如果函数成功,返回值是一个指定的服务控制管理器数据库的句柄。如果函数失败,返回值为NULL 。要获得更详细的错误信息,可以使用GetLastError 获得错误代码。

    CreateService 函数
    创建一个服务对象,并将其添加到指定的服务控制管理器数据库的函数。
    函数声明
    SC_HANDLE CreateService( SC_HANDLE hSCManager, //服务控制管理程序的数据库句柄 LPCTSTR lpServiceName, //以NULL 结尾的服务名 LPCTSTR lpDisplayName, //以NULL 结尾的服务名 DWORD dwDesiredAccess, //指定服务返回类型 DWORD dwServiceType, //指定服务类型 DWORD dwStartType, //指定何时启动服务 DWORD dwErrorControl, //指定服务启动失败的严重程度 LPCTSTR lpBinaryPathName,//指定服务程序二进制文件的路径 LPCTSTR lpLoadOrderGroup,//指定顺序装入的服务组名 LPDWORD lpdwTagId, //忽略,NULL LPCTSTR lpDependencies, //指定启动该服务前必先启动的服务 LPCTSTR lpServiceStartName,//指定服务帐号 LPCTSTR lpPassword //指定对应的口令);
    参数

    hSCManager服务控制管理器数据库的句柄。 此句柄由OpenSCManager函数返回,并且必须 具有 SC_MANAGER_CREATE_SERVICE 的访问权限。 有关更多的信息请参阅Service安全和访问权限。
    lpServiceName要安装该服务的名称。 最大字符串长度为 256 个字符。 服务控制管理器数据库将保留在的字符的大小写,但是服务名称比较总是区分大小写。 正斜杠和一个反斜线不是有效的服务名称字符。
    lpDisplayName对被用户界面程序用来识别服务的显示名称。 此字符串具有最大长度为 256 个字符。 服务控制管理器中的情况下保留名称。 显示名称比较总是不区分大小写。
    dwDesiredAccess对服务的访问。 请求的访问之前,系统将检查调用进程的访问令牌。 一个值列表请参阅服务安全和访问权限。
    dwServiceType服务类型。 此参数可以是下列值之一:




    VALUE
    MEANING




    SERVICE_ADAPTER
    保留


    SERVICE_FILE_SYSTEM_DRIVER
    文件系统驱动服务程序


    SERVICE_KERNEL_DRIVER
    驱动服务程序


    SERVICE_RECOGNIZER_DRIVER
    保留


    SERVICE_WIN32_OWN_PROCESS
    运行于独立进程的服务程序


    SERVICE_WIN32_SHARE_PROCESS
    被多个进程共享的服务程序




    若使用了SERVICE_WIN32_OWN_PROCESS 或 SERVICE_WIN32_SHARE_PROCESS且使用LocalSystem帐号来运行该服务程序,则还可以附加使用下面的值:



    VALUE
    MEANING




    SERVICE_INTERACTIVE_PROCESS
    该服务可以与桌面程序进行交互操作




    dwStartType服务启动选项。此参数可以是下列值之一:



    VALUE
    MEANING




    SERVICE_AUTO_START
    系统启动时由服务控制管理器自动启动该服务程序


    SERVICE_BOOT_START
    用于由系统加载器创建的设备驱动程序, 只能用于驱动服务程序


    SERVICE_DEMAND_START
    由服务控制管理器(SCM)启动的服务


    SERVICE_DISABLED
    表示该服务不可启动


    SERVICE_SYSTEM_START
    用于由IoInitSystem函数创建的设备驱动程序




    dwErrorControl当该启动服务失败时产生错误的严重程度以及采取的保护措施。
    lpBinaryPathName服务程序二进制文件,完全限定路径。 如果路径中包含空格它必须被引用,以便它正确的解析。
    lpLoadOrderGroup在加载顺序此服务所属的组的名称。
    lpdwTagId指向接收 lpLoadOrderGroup 参数中指定的组中唯一的标记值的变量。
    lpDependencies空分隔名称的服务或加载顺序组系统必须在这个服务开始之前的双空终止数组的指针。 如果服务没有任何依赖关系,请指定为 NULL 或空字符串。
    lpServiceStartName该服务应在其下运行的帐户的名称。
    lpPassword由lpServiceStartName参数指定的帐户名的密码。

    返回值

    如果函数成功,返回值将是该服务的句柄。如果函数失败,则返回值为 NULL。 若要扩展的错误了解调用GetLastError。

    OpenService 函数
    打开一个已经存在的服务。
    函数声明
    SC_HANDLE WINAPI OpenService( _In_ SC_HANDLE hSCManager, _In_ LPCTSTR lpServiceName, _In_ DWORD dwDesiredAccess);
    参数

    hSCManager
    SCM数据库句柄。
    lpServiceName
    要打开服务的名字,这和CreateService形参lpServiceName一样,不是服务显示名称。
    dwDesiredAccess
    服务权限。

    返回值

    成功,返回服务句柄;失败返回NULL,可以通过GetLastError获取错误码。

    StartService 函数
    启动服务。
    函数声明
    BOOL WINAPI StartService( _In_ SC_HANDLE hService, _In_ DWORD dwNumServiceArgs, _In_opt_ LPCTSTR *lpServiceArgVectors);
    参数

    hService
    OpenService 或CreateService function返回的服务句柄,需要有SERVICE_START权限。
    dwNumServiceArgs
    下一个形参lpServiceArgVectors的字符串个数,如果lpServiceArgVectors为空,那么该参数设为0。
    lpServiceArgVectors
    传递给服务ServiceMain的参数,如果没有,可以设为NULL;否则,第一个形参lpServiceArgVectors[0]为服务的名字,其他则为需要传入的参数。

    注意

    驱动不接受这些参数,即lpServiceArgVectors为空,dwNumServiceArgs为0。
    返回值

    成功,返回非0;失败,返回0,错误码使用GetLastError获得。

    ControlService函数
    给指定的服务发送一个控制码。
    函数声明
    BOOL WINAPI ControlService( _In_ SC_HANDLE hService, _In_ DWORD dwControl, _Out_ LPSERVICE_STATUS lpServiceStatus);
    参数

    hService
    OpenService 或CreateService function返回的服务句柄;
    dwControl
    要发送的控制码,控制码可以是下列系统定义的控制码;也可以是用户自定义的控制码,范围是128-255,需要有SERVICE_USER_DEFINED_CONTROL权限。
    lpServiceStatus
    返回一个指向存储服务最新状态的结构体ServiceStatus,返回的服务信息来自SCM中最近的服务状态报告。

    返回值

    成功,返回非0;失败,返回0,错误码使用GetLastError获得。

    DeleteService 函数
    从服务控制管理器数据库中删除指定的服务。
    函数声明
    BOOL WINAPI DeleteService( _In_ SC_HANDLE hService);
    参数

    hService
    [输入]服务的句柄,可以由OpenService 或者 CreateService 函数返回,而且该服务必须要有删除权限。

    返回值

    成功,返回非0;失败,返回0,错误码使用GetLastError获得。

    StartServiceCtrlDispatcher 函数
    将服务进程的主线程连接到服务控制管理器,该线程将线程作为调用过程的服务控制分派器线程。
    函数声明
    BOOL WINAPI StartServiceCtrlDispatcher( _In_ const SERVICE_TABLE_ENTRY *lpServiceTable);
    参数

    lpServiceTable
    [输入]指向一系列SERVICE_TABLE_ENTRY结构的指针,其中包含可在调用进程中执行的每个服务的一个条目。 表中最后一个条目的成员必须具有NULL值来指定表的结尾。

    返回值

    成功,返回非0;失败,返回0,错误码使用GetLastError获得。

    RegisterServiceCtrlHandler 函数实现原理创建自启动服务的原理是:

    打开服务控制管理器数据库并获取数据库的句柄

    若要创建服务的操作,则开始创建服务,设置SERVICE_AUTO_START开机自启动;若是其他操作,则打开服务,获取服务句柄
    然后,根据服务句柄,若操作是启动,则使用StartService启动服务;若服务是停止,则使用ControlService停止服务;若操作是删除,则使用DeleteService删除服务
    关闭服务句柄和服务控制管理器数据库句柄

    其中,设置自启动服务启动的程序,并不是普通的程序,而是要求程序要创建了服务入口点函数,否则,不能创建系统服务。创建服务程序的原理是是:
    服务程序主函数(main)调用系统函数StartServiceCtrlDispatcher连接程序主线程到服务控制管理程序(其中定义了服务入口点函数是ServiceMain)。服务控制管理程序启动服务程序后,等待服务程序主函数调用StartServiceCtrlDispatcher。如果没有调用该函数,设置服务入口点,则会报错。
    执行服务初始化任务(同时执行多个服务的服务有多个入口点函数),首先调用RegisterServiceCtrlHandler定义控制处理程序函数(本例中是ServiceCtrlHandle),初始化后通过SetServiceStatus设定进行运行状态,然后运行服务代码。
    控制处理程序(Handler)在服务收到控制请求时由控制分发线程引用(最少要有停止服务的能力)。
    编码实现创建系统服务// 0 加载服务 1 启动服务 2 停止服务 3 删除服务BOOL SystemServiceOperate(char *lpszDriverPath, int iOperateType){ BOOL bRet = TRUE; char szName[MAX_PATH] = { 0 }; ::lstrcpy(szName, lpszDriverPath); // 过滤掉文件目录,获取文件名 ::PathStripPath(szName); SC_HANDLE shOSCM = NULL, shCS = NULL; SERVICE_STATUS ss; DWORD dwErrorCode = 0; BOOL bSuccess = FALSE; // 打开服务控制管理器数据库 shOSCM = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); if (!shOSCM) { ShowError("OpenSCManager"); return FALSE; } if (0 != iOperateType) { // 打开一个已经存在的服务 shCS = OpenService(shOSCM, szName, SERVICE_ALL_ACCESS); if (!shCS) { ShowError("OpenService"); ::CloseServiceHandle(shOSCM); shOSCM = NULL; return FALSE; } } switch (iOperateType) { case 0: { // 创建服务 // SERVICE_AUTO_START 随系统自动启动 // SERVICE_DEMAND_START 手动启动 shCS = ::CreateService(shOSCM, szName, szName, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL, lpszDriverPath, NULL, NULL, NULL, NULL, NULL); if (!shCS) { ShowError("CreateService"); bRet = FALSE; } break; } case 1: { // 启动服务 if (!::StartService(shCS, 0, NULL)) { ShowError("StartService"); bRet = FALSE; } break; } case 2: { // 停止服务 if (!::ControlService(shCS, SERVICE_CONTROL_STOP, &ss)) { ShowError("ControlService"); bRet = FALSE; } break; } case 3: { // 删除服务 if (!::DeleteService(shCS)) { ShowError("DeleteService"); bRet = FALSE; } break; } default: break; } // 关闭句柄 if (shCS) { ::CloseServiceHandle(shCS); shCS = NULL; } if (shOSCM) { ::CloseServiceHandle(shOSCM); shOSCM = NULL; } return bRet;}
    创建服务程序int _tmain(int argc, _TCHAR* argv[]){ // 注册服务入口函数 SERVICE_TABLE_ENTRY stDispatchTable[] = { { g_szServiceName, (LPSERVICE_MAIN_FUNCTION)ServiceMain }, { NULL, NULL } }; ::StartServiceCtrlDispatcher(stDispatchTable); return 0;}
    void __stdcall ServiceMain(DWORD dwArgc, char *lpszArgv){ g_ServiceStatus.dwServiceType = SERVICE_WIN32; g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING; g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP; g_ServiceStatus.dwWin32ExitCode = 0; g_ServiceStatus.dwServiceSpecificExitCode = 0; g_ServiceStatus.dwCheckPoint = 0; g_ServiceStatus.dwWaitHint = 0; g_ServiceStatusHandle = ::RegisterServiceCtrlHandler(g_szServiceName, ServiceCtrlHandle); g_ServiceStatus.dwCurrentState = SERVICE_RUNNING; g_ServiceStatus.dwCheckPoint = 0; g_ServiceStatus.dwWaitHint = 0; ::SetServiceStatus(g_ServiceStatusHandle, &g_ServiceStatus); // 自己程序实现部分代码放在这里 // !!注意!! 此处一定要为死循环, 否则在关机再开机的情况(不是点击重启), 不能创建用户进程 while (TRUE) { Sleep(5000); DoTask(); }}
    void __stdcall ServiceCtrlHandle(DWORD dwOperateCode){ switch (dwOperateCode) { case SERVICE_CONTROL_PAUSE: { // 暂停 g_ServiceStatus.dwCurrentState = SERVICE_PAUSED; break; } case SERVICE_CONTROL_CONTINUE: { // 继续 g_ServiceStatus.dwCurrentState = SERVICE_RUNNING; break; } case SERVICE_CONTROL_STOP: { // 停止 g_ServiceStatus.dwWin32ExitCode = 0; g_ServiceStatus.dwCurrentState = SERVICE_STOPPED; g_ServiceStatus.dwCheckPoint = 0; g_ServiceStatus.dwWaitHint = 0; ::SetServiceStatus(g_ServiceStatusHandle, &g_ServiceStatus); break; } case SERVICE_CONTROL_INTERROGATE: { // 询问 break; } default: break; }}
    程序测试在 main 函数中调用上述封装好的函数接口,进行测试。main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ BOOL bRet = FALSE; char szFileName[MAX_PATH] = "C:\\Users\\DemonGan\\Desktop\\AutoRun_Service_Test\\Debug\\ServiceTest.exe"; // 创建并启动服务 bRet = SystemServiceOperate(szFileName, 0); if (FALSE == bRet) { printf("Create Error!\n"); } bRet = SystemServiceOperate(szFileName, 1); if (FALSE == bRet) { printf("Start Error!\n"); } printf("Create and Start OK.\n"); system("pause"); // 停止并删除服务 bRet = SystemServiceOperate(szFileName, 2); if (FALSE == bRet) { printf("Stop Error!\n"); } bRet = SystemServiceOperate(szFileName, 3); if (FALSE == bRet) { printf("Delete Error!\n"); } printf("Stop and Delete OK.\n"); system("pause"); return 0;}
    测试结果:
    “以管理员身份运行程序”打开程序,提示系统服务创建并启动成功。

    查看任务管理器,发现“ServiceTest.exe”进程已经启动。

    接着,打开服务管理器,查看服务,“ServiceTest.exe”服务成功创建。

    查看服务属性,路径和启动类型正确。

    执行服务停止删除,程序提示执行成功。检查任务管理器和服务管理器,均为发现“ServiceTest.exe”服务,所以,删除成功。

    总结注意,创建系统服务,要求要有管理员权限。其中,创建系统服务启动的程序,它需要额外创建服务入口点函数ServiceMain,这样才能创建服务成功,否则会报错。
    系统服务是运行在Session 0,系统服务的Session 0隔离,阻断了系统服务和用户桌面进程之间进行交互和通信的桥梁。各个Session之间是相互独立的。在不同Session中运行的实体,相互之间不能发送Windows消息、共享UI元素或者是在没有指定他们有权限访问全局名字空间(并且提供正确的访问控制设置)的情况下,共享核心对象。这样也就是为什么,在系统服务不能显示程序界面,也不能用常规的方式创建有界面的进程。要想创建带界面的进程,要使用另外的方式,我已经写成相应的文档分享给大家了,大家可以搜索我写的相关文档来参考。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-11-24 17:08:53
显示 45 到 60 ,共 15 条
eject