微信公众平台开发教程(五)自定义菜单
一、概述:
如果只有输入框,可能太简单,感觉像命令行。自定义菜单,给我们提供了很大的灵活性,更符合用户的操作习惯。在一个小小的微信对话页面,可以实现更多的功能。菜单直观明了,不仅能提供事件响应,还支持URL跳转,如果需要的功能比较复杂,我们大可以使用URL跳转,跳转至我们的网页即可。
注意:自定义菜单,只有服务号才有此功能
如何注册,见第一章:微信公众账号开发教程(一) 基本原理及微信公众账号注册
接着我们详细介绍,如何实现自定义菜单?
二、详细步骤:
1、首先获取access_token
access_token是公众号的全局唯一票据,公众号调用各接口时都需使用access_token。正常情况下access_token有效期为7200秒,重复获取将导致上次获取的access_token失效。
公众号可以使用AppID和AppSecret调用本接口来获取access_token。AppID和AppSecret可在开发模式中获得(需要已经成为开发者,且帐号没有异常状态)。注意调用所有微信接口时均需使用https协议。
接口调用请求说明
http请求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
参数说明
参数 | 是否必须 | 说明 |
---|---|---|
grant_type | 是 | 获取access_token填写client_credential |
appid | 是 | 第三方用户唯一凭证 |
secret | 是 | 第三方用户唯一凭证密钥,既appsecret |
返回说明
正常情况下,微信会返回下述JSON数据包给公众号:
{"access_token":"ACCESS_TOKEN","expires_in":7200}
参数 | 说明 |
---|---|
access_token | 获取到的凭证 |
expires_in | 凭证有效时间,单位:秒 |
错误时微信会返回错误码等信息,JSON数据包示例如下(该示例为AppID无效错误):
{"errcode":40013,"errmsg":"invalid appid"}
2、创建自定义菜单
自定义菜单能够帮助公众号丰富界面,让用户更好更快地理解公众号的功能。开启自定义菜单后,公众号界面如图所示:
目前自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。一级菜单最多4个汉字,二级菜单最多7个汉字,多出来的部分将会以“...”代替。请注意,创建自定义菜单后,由于微信客户端缓存,需要24小时微信客户端才会展现出来。建议测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。
目前自定义菜单接口可实现两种类型按钮,如下:
click: 用户点击click类型按钮后,微信服务器会通过消息接口推送消息类型为event 的结构给开发者(参考消息接口指南),并且带上按钮中开发者填写的key值,开发者可以通过自定义的key值与用户进行交互; view: 用户点击view类型按钮后,微信客户端将会打开开发者在按钮中填写的url值 (即网页链接),达到打开网页的目的,建议与网页授权获取用户基本信息接口结合,获得用户的登入个人信息。
接口调用请求说明
http请求方式:POST(请使用https协议) https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN
请求示例
1 { 2 "button":[ 3 { 4 "type":"click", 5 "name":"今日歌曲", 6 "key":"V1001_TODAY_MUSIC" 7 }, 8 { 9 "type":"click", 10 "name":"歌手简介", 11 "key":"V1001_TODAY_SINGER" 12 }, 13 { 14 "name":"菜单", 15 "sub_button":[ 16 { 17 "type":"view", 18 "name":"搜索", 19 "url":"http://www.soso.com/" 20 }, 21 { 22 "type":"view", 23 "name":"视频", 24 "url":"http://v.qq.com/" 25 }, 26 { 27 "type":"click", 28 "name":"赞一下我们", 29 "key":"V1001_GOOD" 30 }] 31 }] 32 }
参数说明
参数 | 是否必须 | 说明 |
---|---|---|
button | 是 | 一级菜单数组,个数应为1~3个 |
sub_button | 否 | 二级菜单数组,个数应为1~5个 |
type | 是 | 菜单的响应动作类型,目前有click、view两种类型 |
name | 是 | 菜单标题,不超过16个字节,子菜单不超过40个字节 |
key | click类型必须 | 菜单KEY值,用于消息接口推送,不超过128字节 |
url | view类型必须 | 网页链接,用户点击菜单可打开链接,不超过256字节 |
返回结果
正确时的返回JSON数据包如下:
{"errcode":0,"errmsg":"ok"}
错误时的返回JSON数据包如下(示例为无效菜单名长度):
{"errcode":40018,"errmsg":"invalid button name size"}
3、查询菜单
使用接口创建自定义菜单后,开发者还可使用接口查询自定义菜单的结构。
请求说明
http请求方式:GET https://api.weixin.qq.com/cgi-bin/menu/get?access_token=ACCESS_TOKEN
返回说明
对应创建接口,正确的Json返回结果: {"menu":{"button":[{"type":"click","name":"今日歌曲","key":"V1001_TODAY_MUSIC","sub_button":[]},{"type":"click","name":"歌手简介","key":"V1001_TODAY_SINGER","sub_button":[]},{"name":"菜单","sub_button":[{"type":"view","name":"搜索","url":"http://www.soso.com/","sub_button":[]},{"type":"view","name":"视频","url":"http://v.qq.com/","sub_button":[]},{"type":"click","name":"赞一下我们","key":"V1001_GOOD","sub_button":[]}]}]}}
4、删除菜单
使用接口创建自定义菜单后,开发者还可使用接口删除当前使用的自定义菜单。
请求说明
http请求方式:GET https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=ACCESS_TOKEN
返回说明
对应创建接口,正确的Json返回结果: {"errcode":0,"errmsg":"ok"}
5、事件处理
用户点击自定义菜单后,如果菜单按钮设置为click类型,则微信会把此次点击事件推送给开发者,注意view类型(跳转到URL)的菜单点击不会上报。
推送XML数据包示例:
1 <xml> 2 <ToUserName><![CDATA[toUser]]></ToUserName> 3 <FromUserName><![CDATA[FromUser]]></FromUserName> 4 <CreateTime>123456789</CreateTime> 5 <MsgType><![CDATA[event]]></MsgType> 6 <Event><![CDATA[CLICK]]></Event> 7 <EventKey><![CDATA[EVENTKEY]]></EventKey> 8 </xml>
参数说明:
参数 | 描述 |
---|---|
ToUserName | 开发者微信号 |
FromUserName | 发送方帐号(一个OpenID) |
CreateTime | 消息创建时间 (整型) |
MsgType | 消息类型,event |
Event | 事件类型,CLICK |
EventKey | 事件KEY值,与自定义菜单接口中KEY值对应 |
三、实例讲解
还接着上一篇文章讲。微信公众账号开发教程(三) 实例入门:机器人(附源码)
我们将在上一篇文章基础上,添加自定义菜单功能。
1、获取access_token
首先需要得到AppId和AppSecret
当你成为开发者后,自然能够在,开发者模式,便可看到这两个值,可以重置。
然后调用按照二.1中所示,进行操作。
注意:access_token有过期时间,如果过期,需要重新获取。
代码如下:
1 private static DateTime GetAccessToken_Time; 2 /// <summary> 3 /// 过期时间为7200秒 4 /// </summary> 5 private static int Expires_Period = 7200; 6 /// <summary> 7 /// 8 /// </summary> 9 private static string mAccessToken; 10 /// <summary> 11 /// 12 /// </summary> 13 public static string AccessToken 14 { 15 get 16 { 17 //如果为空,或者过期,需要重新获取 18 if (string.IsNullOrEmpty(mAccessToken) || HasExpired()) 19 { 20 //获取 21 mAccessToken = GetAccessToken(AppID, AppSecret); 22 } 23 24 return mAccessToken; 25 } 26 } 27 /// <summary> 28 /// 29 /// </summary> 30 /// <param name="appId"></param> 31 /// <param name="appSecret"></param> 32 /// <returns></returns> 33 private static string GetAccessToken(string appId, string appSecret) 34 { 35 string url = string.Format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}", appId, appSecret); 36 string result = HttpUtility.GetData(url); 37 38 XDocument doc = XmlUtility.ParseJson(result, "root"); 39 XElement root = doc.Root; 40 if (root != null) 41 { 42 XElement access_token = root.Element("access_token"); 43 if (access_token != null) 44 { 45 GetAccessToken_Time = DateTime.Now; 46 if (root.Element("expires_in")!=null) 47 { 48 Expires_Period = int.Parse(root.Element("expires_in").Value); 49 } 50 return access_token.Value; 51 } 52 else 53 { 54 GetAccessToken_Time = DateTime.MinValue; 55 } 56 } 57 58 return null; 59 } 60 /// <summary> 61 /// 判断Access_token是否过期 62 /// </summary> 63 /// <returns>bool</returns> 64 private static bool HasExpired() 65 { 66 if (GetAccessToken_Time != null) 67 { 68 //过期时间,允许有一定的误差,一分钟。获取时间消耗 69 if (DateTime.Now > GetAccessToken_Time.AddSeconds(Expires_Period).AddSeconds(-60)) 70 { 71 return true; 72 } 73 } 74 return false; 75 }
2、设置菜单
菜单需根据需要,按照实际要求进行设定。
这里我们提供天气查询功能,将常用的城市列出来,点击即可查询。
然后还提供了友情链接,这里提供了view类型的菜单,直接可以跳转至URL页面,为跳转做个好的演示。
具体菜单如下:
1 { 2 "button": [ 3 { 4 "name": "链接", 5 "sub_button": [ 6 { 7 "type": "view", 8 "name": "搜索", 9 "url": "http://www.baidu.com/" 10 }, 11 { 12 "type": "view", 13 "name": "视频", 14 "url": "http://v.qq.com/" 15 }, 16 { 17 "type": "click", 18 "name": "赞一下我们", 19 "key": "BTN_GOOD" 20 } 21 ] 22 }, 23 { 24 "name": "查询天气", 25 "sub_button": [ 26 { 27 "type": "click", 28 "name": "武汉", 29 "key": "BTN_TQ_WUHAN" 30 }, 31 { 32 "type": "click", 33 "name": "上海", 34 "key": "BTN_TQ_SHANGHAI" 35 }, 36 { 37 "type": "click", 38 "name": "北京", 39 "key": "BTN_TQ_BEIJING" 40 } 41 ] 42 }, 43 { 44 "type": "click", 45 "name": "帮助", 46 "key": "BTN_HELP" 47 } 48 ] 49 }
3、管理菜单
因为菜单的变更没有那么频繁,因此通过txt文件来设置菜单,并通过管理界面来管理菜单。
主要的管理功能:
1)从文件加载菜单
2)创建菜单。即将菜单通知微信服务端,并更新至微信客户端
3)查询菜单。获取当前系统的菜单。
4)删除菜单。从微信服务器删除菜单,也可以删除后再创建。
实现代码如下:
1 public class MenuManager 2 { 3 /// <summary> 4 /// 菜单文件路径 5 /// </summary> 6 private static readonly string Menu_Data_Path = System.AppDomain.CurrentDomain.BaseDirectory + "/Data/menu.txt"; 7 /// <summary> 8 /// 获取菜单 9 /// </summary> 10 public static string GetMenu() 11 { 12 string url = string.Format("https://api.weixin.qq.com/cgi-bin/menu/get?access_token={0}", Context.AccessToken); 13 14 return HttpUtility.GetData(url); 15 } 16 /// <summary> 17 /// 创建菜单 18 /// </summary> 19 public static void CreateMenu(string menu) 20 { 21 string url = string.Format("https://api.weixin.qq.com/cgi-bin/menu/create?access_token={0}", Context.AccessToken); 22 //string menu = FileUtility.Read(Menu_Data_Path); 23 HttpUtility.SendHttpRequest(url, menu); 24 } 25 /// <summary> 26 /// 删除菜单 27 /// </summary> 28 public static void DeleteMenu() 29 { 30 string url = string.Format("https://api.weixin.qq.com/cgi-bin/menu/delete?access_token={0}", Context.AccessToken); 31 HttpUtility.GetData(url); 32 } 33 /// <summary> 34 /// 加载菜单 35 /// </summary> 36 /// <returns></returns> 37 public static string LoadMenu() 38 { 39 return FileUtility.Read(Menu_Data_Path); 40 } 41 }
4、基本方法
上面的代码,其实我们对一些公共功能做了封装。如进行get请求、POST提交等操作,读取文件等。
这里我们提供进行get、Post提交的方法案例代码,如果使用,建议优化。
1 using System; 2 using System.IO; 3 using System.Net; 4 using System.Text; 5 using System.Web; 6 7 namespace Yank.WeiXin.Robot.Utility 8 { 9 /// <summary> 10 /// Http帮助类 11 /// </summary> 12 class HttpUtility 13 { 14 /// <summary> 15 /// 发送请求 16 /// </summary> 17 /// <param name="url">Url地址</param> 18 /// <param name="data">数据</param> 19 public static string SendHttpRequest(string url, string data) 20 { 21 return SendPostHttpRequest(url,"application/x-www-form-urlencoded",data); 22 } 23 /// <summary> 24 /// 25 /// </summary> 26 /// <param name="url"></param> 27 /// <returns></returns> 28 public static string GetData(string url) 29 { 30 return SendGetHttpRequest(url,"application/x-www-form-urlencoded"); 31 } 32 33 /// <summary> 34 /// 发送请求 35 /// </summary> 36 /// <param name="url">Url地址</param> 37 /// <param name="method">方法(post或get)</param> 38 /// <param name="method">数据类型</param> 39 /// <param name="requestData">数据</param> 40 public static string SendPostHttpRequest(string url,string contentType,string requestData) 41 { 42 WebRequest request = (WebRequest)HttpWebRequest.Create(url); 43 request.Method = "POST"; 44 byte[] postBytes = null; 45 request.ContentType = contentType; 46 postBytes = Encoding.UTF8.GetBytes(requestData); 47 request.ContentLength = postBytes.Length; 48 using (Stream outstream = request.GetRequestStream()) 49 { 50 outstream.Write(postBytes, 0, postBytes.Length); 51 } 52 string result = string.Empty; 53 using (WebResponse response = request.GetResponse()) 54 { 55 if (response != null) 56 { 57 using (Stream stream = response.GetResponseStream()) 58 { 59 using (StreamReader reader = new StreamReader(stream, Encoding.UTF8)) 60 { 61 result = reader.ReadToEnd(); 62 } 63 } 64 65 } 66 } 67 return result; 68 } 69 70 /// <summary> 71 /// 发送请求 72 /// </summary> 73 /// <param name="url">Url地址</param> 74 /// <param name="method">方法(post或get)</param> 75 /// <param name="method">数据类型</param> 76 /// <param name="requestData">数据</param> 77 public static string SendGetHttpRequest(string url, string contentType) 78 { 79 WebRequest request = (WebRequest)HttpWebRequest.Create(url); 80 request.Method = "GET"; 81 request.ContentType = contentType; 82 string result = string.Empty; 83 using (WebResponse response = request.GetResponse()) 84 { 85 if (response != null) 86 { 87 using (Stream stream = response.GetResponseStream()) 88 { 89 using (StreamReader reader = new StreamReader(stream, Encoding.UTF8)) 90 { 91 result = reader.ReadToEnd(); 92 } 93 } 94 } 95 } 96 return result; 97 } 98 } 99 }
1 using System; 2 using System.Xml.Linq; 3 using Newtonsoft.Json; 4 5 namespace Yank.WeiXin.Robot.Utility 6 { 7 class XmlUtility 8 { 9 /// <summary> 10 /// 11 /// </summary> 12 /// <param name="json"></param> 13 /// <param name="rootName"></param> 14 /// <returns></returns> 15 public static XDocument ParseJson(string json,string rootName) 16 { 17 return JsonConvert.DeserializeXNode(json, rootName); 18 } 19 } 20 }
5、事件处理
设置了菜单,这下需要处理事件了。跟我们之前设计ASPX或者WinForm一样,都要绑定按钮的事件。这里只是通过XML消息将请求传递过来。
通过“2、设置菜单"中具体的菜单内容,我们便已经知道需要进行哪些事件处理了。对于按钮类型为view的,无须处理,它会自动跳转至指定url.
需要处理的点击事件:
1)赞一下
2)查询某城市的天气,北京、上海、武汉
3)帮助
这个还要沿用上章中的事件处理器EventHandler来扩展处理。
具体的实现代码吧:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using Yank.WeiXin.Robot.Messages; 6 7 namespace Yank.WeiXin.Robot.Handlers 8 { 9 class EventHandler : IHandler 10 { 11 /// <summary> 12 /// 请求的xml 13 /// </summary> 14 private string RequestXml { get; set; } 15 /// <summary> 16 /// 构造函数 17 /// </summary> 18 /// <param name="requestXml"></param> 19 public EventHandler(string requestXml) 20 { 21 this.RequestXml = requestXml; 22 } 23 /// <summary> 24 /// 处理请求 25 /// </summary> 26 /// <returns></returns> 27 public string HandleRequest() 28 { 29 string response = string.Empty; 30 EventMessage em = EventMessage.LoadFromXml(RequestXml); 31 if (em != null) 32 { 33 switch (em.Event.ToLower()) 34 { 35 case ("subscribe"): 36 response = SubscribeEventHandler(em); 37 break; 38 case "click": 39 response = ClickEventHandler(em); 40 break; 41 } 42 } 43 44 return response; 45 } 46 /// <summary> 47 /// 关注 48 /// </summary> 49 /// <param name="em"></param> 50 /// <returns></returns> 51 private string SubscribeEventHandler(EventMessage em) 52 { 53 //回复欢迎消息 54 TextMessage tm = new TextMessage(); 55 tm.ToUserName = em.FromUserName; 56 tm.FromUserName = em.ToUserName; 57 tm.CreateTime = Common.GetNowTime(); 58 tm.Content = "欢迎您关注***,我是大哥大,有事就问我,呵呵!\n\n"; 59 return tm.GenerateContent(); 60 } 61 /// <summary> 62 /// 处理点击事件 63 /// </summary> 64 /// <param name="em"></param> 65 /// <returns></returns> 66 private string ClickEventHandler(EventMessage em) 67 { 68 string result = string.Empty; 69 if (em != null && em.EventKey != null) 70 { 71 switch (em.EventKey.ToUpper()) 72 { 73 case "BTN_GOOD": 74 result = btnGoodClick(em); 75 break; 76 case "BTN_TQ_BEIJING": 77 result = searchWeather("beijing", em); 78 break; 79 case "BTN_TQ_SHANGHAI": 80 result = searchWeather("shanghai", em); 81 break; 82 case "BTN_TQ_WUHAN": 83 result = searchWeather("wuhai", em); 84 break; 85 case "BTN_HELP": 86 result = btnHelpClick(em); 87 break; 88 } 89 } 90 91 return result; 92 } 93 /// <summary> 94 /// 赞一下 95 /// </summary> 96 /// <param name="em"></param> 97 /// <returns></returns> 98 private string btnGoodClick(EventMessage em) 99 { 100 //回复欢迎消息 101 TextMessage tm = new TextMessage(); 102 tm.ToUserName = em.FromUserName; 103 tm.FromUserName = em.ToUserName; 104 tm.CreateTime = Common.GetNowTime(); 105 tm.Content = @"谢谢您的支持!"; 106 return tm.GenerateContent(); 107 } 108 /// <summary> 109 /// 帮助 110 /// </summary> 111 /// <param name="em"></param> 112 /// <returns></returns> 113 private string btnHelpClick(EventMessage em) 114 { 115 //回复欢迎消息 116 TextMessage tm = new TextMessage(); 117 tm.ToUserName = em.FromUserName; 118 tm.FromUserName = em.ToUserName; 119 tm.CreateTime = Common.GetNowTime(); 120 tm.Content = @"查询天气,输入tq 城市名称\拼音\首字母"; 121 return tm.GenerateContent(); 122 } 123 /// <summary> 124 /// 查询天气 125 /// </summary> 126 /// <param name="cityName"></param> 127 /// <param name="em"></param> 128 /// <returns></returns> 129 private string searchWeather(string cityName, EventMessage em) 130 { 131 TextMessage tm = new TextMessage(); 132 tm.Content = WeatherHelper.GetWeather(cityName); 133 //进行发送者、接收者转换 134 tm.ToUserName = em.FromUserName; 135 tm.FromUserName = em.ToUserName; 136 tm.CreateTime = Common.GetNowTime(); 137 return tm.GenerateContent(); 138 } 139 } 140 }
6、效果图
终于大工告成,最后来看下效果图吧
文章转载自 http://www.cnblogs.com/yank/p/3418194.html