惨案:主题文件爆炸

刚搬完网站,发现了一个小问题,就是最新版chrome无法正常打开博客后台,而且无法正常加载ajax切换的结果,显示加载失败

今天终于发现了原因:是主题的问题,但是我不会修(摔!

所以在新主题完成之前,就先用官方主题凑合吧,所以友链也没有了,大家见谅

用C#来实现QQ宠物自动挂机和投喂功能

其实这个想法在初中的时候就有了,当时因为什么都不会,所以也没有去做这个东西

昨天下午十分无聊,突然想起来了这件事,便打开vs写了一波

0x00、基本原理:

这个东西是利用3gqq来实现的,3gqq几乎都是纯文本,也方便进行封装来实现功能

在几年前,3gqq是使用sid来存储用户的登陆信息的,但是我昨天测试了一下,现在已经改为用cookie来保存登陆信息(虽然在网址里也保存了sid参数)

打开http://pet.3g.qq.com/后的界面:

我们可以很轻松地找到真正用来存储登陆信息的两个cookie值,后面的测试也证实了这一点:

我们记下这个uin和skey的值,在后面会经常用到

0x01、构造http请求函数:

我们可以根据上面来编写获取信息的方法:

/// <summary>  
/// GET 请求与获取结果  
/// </summary>  
public static string HttpGetPet(string Url, string postDataStr, string uin, string skey)
{
    try
    {
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(Url + (postDataStr == "" ? "" : "?") + postDataStr);
        request.Method = "GET";
        request.ContentType = "text/html;charset=UTF-8";
        request.Headers.Add("cookie", "uin=" + uin + "; skey=" + skey);
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        Stream myResponseStream = response.GetResponseStream();
        StreamReader myStreamReader = new StreamReader(myResponseStream, Encoding.UTF8);
        string retString = myStreamReader.ReadToEnd();
        myStreamReader.Close();
        myResponseStream.Close();
        return retString;
    }
    catch { }
    return "";
}

可以看到参数里有uin和skey的值,我们可以测试一下:

string html = HttpGetPet("http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/phone_pet", "", "你的uin", "你的skey");
Console.WriteLine(html);
Console.ReadLine();

获取结果:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml">
<wml>
<head>
<meta http-equiv="cache-control" content="no-cache"/>
</head>
    <card id="mainid" title="手机QQ宠物" ontimer="http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/phone_pet?&amp;petid=281914025101042&amp;g_f=0&amp;cmd=1">
    <timer value="9000"/>
    <p><a href="http://app.z.qq.com/app_list.jsp?B_UID=&amp;">应用</a>&gt;手机QQ宠物</p>
        <p>状态 | <a href="http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/phone_pet?&amp;petid=281914025101042&amp;g_f=0&amp;cmd=2">资料</a> | <a href="http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/phone_pet?&amp;petid=281914025101042&amp;g_f=0&amp;cmd=3">喂养</a> | <a href="http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/phone_pet?&amp;petid=281914025101042&amp;g_f=0&amp;cmd=4">粉钻特权</a></p>
        <p>

</p>
<p>
<a href="http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/phone_pet?&amp;petid=281914025101042&amp;g_f=0&amp;cmd=30&amp;gift=0">我的礼包</a>&nbsp;&nbsp;<a href="http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/invite?&amp;petid=281914025101042&amp;g_f=0">好友邀请</a>
</p>
<p>
<img src="http://img.pet.qq.com/phone_pet/img/index12.gif" alt="QQ宠物"/>
</p>
<p>
   我不是一个人看书,旅行,吃饭,因为有晨旭的陪伴!
</p>

<p>
    昵称:pet <img src="http://img.pet.qq.com/phone_pet/LV1_0.gif" alt="VIP"/> <a href="http://sqq.3g.qq.com/s?aid=bizp&amp;pt=page&amp;pc=qzonepetvip&amp;sid=%s"><img src="http://img.pet.qq.com/phone_pet/sq/L1_0.gif" alt="SUPERQ"/></a><br/>
    QQ号:961726194 <br/>
    等级:47<br/>
    状态:空闲

</p>
<p>
    <a href="http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/phone_pet?&amp;petid=281914025101042&amp;g_f=0&amp;cmd=24">宠 物日志</a>
</p>
<p>
    成长值:926418/955400<br/>
    饥饿值:3570/6000 <br/>
    清洁值:3616/6000  <br/>
    健康值:5/5      <br/>
    心情:1000/1000 兴奋<br/>
    成长速度: 260/小时  <a href="http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/phone_pet?&amp;petid=281914025101042&amp;g_f=0&amp;cmd=4">粉钻可加速</a><br/>

    刷新时间(分钟):<input type="text" size="2" maxlength="2" format="*N" value="15" name="curtime"/>
    <anchor>
        确定
        <go href="http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/phone_pet?&amp;petid=281914025101042&amp;g_f=0&amp;cmd=1" method="post">
            <postfield name="freshtime" value="$curtime"/>
        </go>
    </anchor>
    <anchor>
        刷新
        <go href="http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/phone_pet?&amp;petid=281914025101042&amp;g_f=0&amp;cmd=1" method="post">
            <postfield name="freshtime" value="$curtime"/>
        </go>
    </anchor>
</p>



        <p><a href="http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/phone_pet?&amp;petid=281914025101042&amp;g_f=0&amp;cmd=5">学习</a> | <a href="http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/phone_pet?&amp;petid=281914025101042&amp;g_f=0&amp;cmd=6">打工</a> | <a href="http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/phone_pet?&amp;petid=281914025101042&amp;g_f=0&amp;cmd=7">旅游</a> | <a href="http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/farm?&amp;petid=281914025101042&amp;g_f=0&amp;cmd=10">农场</a> | <a href="http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/fish?&amp;petid=281914025101042&amp;g_f=0&amp;cmd=11">渔场</a> | <a href="http://dld.qzapp.z.qq.com/qpet/cgi-bin/phonepk?cmd=index&amp;&amp;zapp_uin=961726194&amp;from=qqpet">大乐斗</a> | <a href="http://pet.gamebbs.qq.com/forum.php?&amp;">论坛</a> </p>

    <p> <a href="http://info60.z.qq.com/infocenter_v2.jsp?B_UID=0">个人中心</a>-<a href="http://blog60.z.qq.com/setting/console.jsp">设置</a>-<a href="http://blog60.z.qq.com/feedback/index.jsp">反馈</a>-<a href="http://blog60.z.qq.com/logout.jsp">退出</a><br/><a href="http://info.3g.qq.com/g/s?aid=index&amp;KqqWap_Act=6&amp;g_f=591&amp;g_ut=1">手机腾讯网</a>-<a href="http://info.3g.qq.com/g/s?aid=navigation&amp;KqqWap_Act=6&amp;g_f=592&amp;g_ut=1">导航</a>-<a href="http://wap.soso.com/navi.jsp?g_f=2119&amp;g_ut=2">搜索</a><br/>我的<a href="http://house60.3g.qq.com/g/s?aid=home_self&amp;KqqWap_Act=6&amp;g_f=595&amp;g_ut=1">家园</a>.<a href="http://f.qq.com/index.php?mod=login&amp;act=qzone&amp;qq=0&amp;&amp;g_f=2455">朋友</a>.<a href="http://ti50.3g.qq.com/g/s?g_f=2507&amp;aid=h&amp;&amp;g_ut=1">微博</a><br/>小Q报时(18:47)<br/> </p>
    </card>
</wml>

如果获取到了正确信息,向上面那样,说明试验成功

0x02、把正则封装好:

根据我的习惯,因为我没有用过dom树解析,所以我在这里用的是正则,封装的函数如下:

/// <summary>
/// 直接获取正则表达式的最后一组匹配值
/// </summary>
/// <param name="str"></param>
/// <param name="regstr"></param>
/// <param name="name"></param>
/// <returns></returns>
public static string Reg_get(string str, string regstr, string name)
{
    string result = "";
    MatchCollection matchs = Reg_solve(str, regstr);
    foreach (Match item in matchs)
    {
        if (item.Success)
        {
            result = item.Groups[name].Value;
        }
    }
    return result;
}
public static MatchCollection Reg_solve(string str, string regstr)
{
    Regex reg = new Regex(regstr, RegexOptions.IgnoreCase);
    return reg.Matches(str);
}

0x03、获取&返回信息:

因为我是放到qq机器人上面用的,所以先设置好统一回复语句:

public static string pleaseLogin = "请设置登陆信息!";
public static string petMore = "更多宠物命令请回复“宠物助手”";

获取基本信息的方法如下:

/// <summary>
/// 获取宠物基本信息
/// </summary>
/// <param name="uin"></param>
/// <param name="skey"></param>
/// <returns></returns>
public static string GetPetState(string uin, string skey)
{
    string result = "";
    string html = HttpGetPet("http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/phone_pet", "", uin, skey);
    html = html.Replace("\r", "");
    html = html.Replace("\n", "");
    if (html.IndexOf("手机统一登录") != -1)
        return pleaseLogin;
    result += Reg_get(html, "alt=\"QQ宠物\"/></p><p>(?<say>.*?)<", "say").Replace(" ", "") + "\r\n"
            + "宠物名:" + Reg_get(html, "昵称:(?<level>.*?)<", "level").Replace(" ", "") + "\r\n"
            + "等级:" + Reg_get(html, "等级:(?<level>.*?)<", "level").Replace(" ", "") + "\r\n"
            + "状态:" + Reg_get(html, "状态:(?<now>.*?)<", "now").Replace(" ", "") + "\r\n"
            + "成长值:" + Reg_get(html, "成长值:(?<now>.*?)<", "now").Replace(" ", "") + "\r\n"
            + "饥饿值:" + Reg_get(html, "饥饿值:(?<now>.*?)<", "now").Replace(" ", "") + "\r\n"
            + "清洁值:" + Reg_get(html, "清洁值:(?<now>.*?)<", "now").Replace(" ", "") + "\r\n"
            + "心情:" + Reg_get(html, "心情:(?<now>.*?)<", "now").Replace(" ", "") + "\r\n" + petMore;
    return result;
}

列出宠物喂养物品:

/// <summary>
/// 喂养宠物选择页面
/// </summary>
/// <param name="uin"></param>
/// <param name="skey"></param>
/// <returns></returns>
public static string FeedPetSelect(string uin, string skey,string page)
{
    string result = "";
    string html = HttpGetPet("http://qqpet.wapsns.3g.qq.com/qqpet/fcgi-bin/phone_pet", "cmd=3&page=" + page, uin, skey);
    html = html.Replace("\r", "");
    html = html.Replace("\n", "");
    if (html.IndexOf("手机统一登录") != -1)
        return pleaseLogin;
    result += "我的账户:" + Reg_get(html, "我的账户:(?<name>.*?)<", "name").Replace(" ", "") + "\r\n";
    int i = 0;
    MatchCollection matchs = Reg_solve(Reg_get(html, "元宝(?<name>.*?)上页", "name").Replace(" ", ""), "<p>(?<name>.*?)<br");
    string[] name = new string[matchs.Count];
    string[] id = new string[matchs.Count];
    foreach (Match item in matchs)
    {
        if (item.Success)
        {
            name[i++] += item.Groups["name"].Value;
        }
    }
    i = 0;
    matchs = Reg_solve(Reg_get(html, "元宝(?<name>.*?)上页", "name").Replace(" ", ""), "goodid=(?<id>.*?)\"");
    foreach (Match item in matchs)
    {
        if (item.Success)
        {
            id[i++] += "物品id:" + item.Groups["id"].Value;
        }
    }
    
    for(int j = 0; j < i;j++)
    {
        result += name[j] + "\r\n" + id[j] + "\r\n";
    }
    if(Reg_get(html, "上页</a>(?<name>.*?)/", "name").Replace(" ", "") != "")
    {
        result += "第" + Reg_get(html, "上页</a>(?<name>.*?)/", "name").Replace(" ", "") + "页,共"
               + Reg_get(html, "上页</a>(.*?)/(?<name>\\d+)", "name").Replace(" ", "") + "页" + "\r\n";
    }
    else
    {
        result += "第" + Reg_get(html, "上页(?<name>.*?)/", "name").Replace(" ", "") + "页,共"
                + Reg_get(html, "上页(.*?)/(?<name>\\d+)", "name").Replace(" ", "") + "页" + "\r\n";
    }
    return result + petMore;
}

基本就是这些了,再贴代码的话,这篇文章的字数就超过最大字数限制了

所有代码都在此处:

https://gist.github.com/chenxuuu/d9d682b58a40ead00250c00c43f1a7f3

欢迎各位大佬进行二次开发,如果开发出来了成果,请在此处留言,谢谢!

【重写】树莓派驱动的b站点播台

之前做的那个挺。。简陋的点播台。。。终于被我废弃了2333

我又写了个新的

已经有的功能为:

弹幕点歌

弹幕点MV

弹幕反馈(发送弹幕)

旧版实现的视频推流功能

自定义介绍字幕

基础的歌词显示

切歌

显示排队歌曲

下载时的cpu温度

闲时随机播放预留歌曲

播放音乐时背景图片随机选择

可点播b站任意视频(会员限制除外)

具体的大家直接去gayhub看readme吧,我写的很详细的~

https://github.com/chenxuuu/24h-raspberry-live-on-bilibili

直播间:http://live.bilibili.com/16703

欢迎捧场哦~

树莓派驱动SSD1331 OLED屏幕

本文内容基本来源于https://luma-oled.readthedocs.io/en/latest/index.html(英文注意)

之前树莓用了一块ssd1306驱动的黑白oled屏,可惜坏掉了,所以我又买了块彩色的(要吃土了

昨天买,今天就到了,好在资料找起来非常顺利,简简单单就找到了可用的库

效果如下:

(↑来自Lumia950XL)

接下来就简单讲下树莓驱动这块彩屏吧,因为有现成的库,所以步骤十分简单

首先是引脚连接,我直接把官方推荐的接发复制过来就好了:

SIP接法:

OLED Pin Name Remarks RPi Pin RPi Function
1 VCC +3.3V Power P01-17 3V3
2 GND Ground P01-20 GND
3 D0 Clock P01-23 GPIO 11 (SCLK)
4 D1 MOSI P01-19 GPIO 10 (MOSI)
5 RST Reset P01-22 GPIO 25
6 DC Data/Command P01-18 GPIO 24
7 CS Chip Select P01-24 GPIO 8 (CE0)

I2C接法:

OLED Pin Name Remarks RPi Pin RPi Function
1 GND Ground P01-6 GND
2 VCC +3.3V Power P01-1 3V3
3 SCL Clock P01-5 GPIO 3 (SCL)
4 SDA Data P01-3 GPIO 2 (SDA)

具体接法解释请点我前往官网文档

我这里用的是SPI接法,另外附一张树莓2B引脚图(去年发过了233):

连好之后,记得开启SPI/I2C接口:

sudo raspi-config

设置完之后,重启:

sudo reboot

接下来安装我们要用到的luma库:

sudo apt-get install python-dev python-pip libfreetype6-dev libjpeg-dev
sudo -H pip install --upgrade pip
sudo apt-get purge python-pip
sudo -H pip install --upgrade luma.oled

一句一句执行完毕,安装成功后,我们接下来装上示例程序:

sudo usermod -a -G i2c,spi,gpio pi
sudo apt-get install python-dev python-pip libfreetype6-dev libjpeg-dev
sudo apt-get install libsdl-dev libportmidi-dev libsdl-ttf2.0-dev libsdl-mixer1.2-dev libsdl-image1.2-dev
sudo -H pip install --upgrade pip setuptools
sudo apt-get purge python-pip
git clone https://github.com/rm-hull/luma.examples.git
cd luma.examples
sudo -H pip install -e .

同样一句一句执行成功后,我们就可以测试了。

cd examples
sudo ./demo.py -d ssd1331 -i spi --width 96 --heig 64
#请根据自己的实际情况更改参数

另一张效果图:

教程完毕

希望本文能让需要本教程的朋友少走一些弯路

使用C#写一个QQ群版像素画游戏

昨天苦命鸳鸯节闲得无聊,就想着能不能抄袭一下之前那个像素画的游戏,经过一下午+一晚上的研究,在昨晚终于把这个小游戏的功能集成到了我的qq机器人里面,效果如下:

这个小游戏的代码实现了以下几个基本功能:

  1. 可以通过指令修改置顶区域的颜色

  2. 颜色完全可以自定义

  3. 判断用户是否是过了五分钟后才再次进行颜色修改的,否则拒绝修改

  4. 每次修改保存一张备份图片

机器人使用了酷Q pro版(air免费版无法发送图片),和C#sdk(https://cqp.cc/t/29261

首先,因为是要修改图片,所以我们需要先在C#工程内引入“System.Drawing

引用完毕后,在开头加上:

using System.Drawing;

修改图片某一个像素点颜色的方法是“SetPixel”,所以构造方法如下:

        public static Bitmap SetPels(Bitmap Pict, Color color, int x, int y, int w, int h)
        {
            //遍历矩形框内的各象素点
            for (int i = x; i < x + w; i++)
            {
                for (int j = y; j < y + h; j++)
                {
                    Pict.SetPixel(i, j, color);//设置当前象素点的颜色
                }
            }
            return Pict;
        }

这样就可以改变指定区域的颜色了,但是qq内发的图片不能精确到指定坐标,毕竟就只是一张图片,所以我把像素点给扩大到了17*17的大小,并且增加了新的方法:

        //x,y限制0-29
        private static Bitmap SetPoint(Bitmap Pict, Color color, int x, int y)
        {
            return SetPels(Pict, color, x * 17, y * 17, 17, 17);
        }

这样就可以方便地修改指定区域了。

我们再加上一个打开文件的方法:

        /// <summary>
        /// 通过FileStream 来打开文件,这样就可以实现不锁定Image文件,到时可以让多用户同时访问Image文件
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public static Bitmap ReadImageFile(string path)
        {
            FileStream fs = File.OpenRead(path); //OpenRead
            int filelength = 0;
            filelength = (int)fs.Length; //获得文件长度 
            Byte[] image = new Byte[filelength]; //建立一个字节数组 
            fs.Read(image, 0, filelength); //按字节流读取 
            System.Drawing.Image result = System.Drawing.Image.FromStream(fs);
            fs.Close();
            Bitmap bit = new Bitmap(result);
            return bit;
        }

有了这些方法,我们就可以进行测试了(提前准备好图片文件):

string picPath = @"C:\Users\Administrator\Desktop\kuqpro\data\image\pixel_game\1.bmp";
Bitmap pic = ReadImageFile(picPath);
pic = SetPoint(pic, color.red, 1, 1);
pic.Save(picPath);

如果不出意外的话,会在左上方出现一个红色方框,而且不是紧贴着边缘的。

我们接下来处理用户时间的问题,首先先使用xml保存配置文件,我写了以下几个方法,效率可能会很低,建议大家使用其他的方法进行替换:

static string path = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
public static string xml_get(long group, string msg)
{
    dircheck(group);
    XElement root = XElement.Load(path + group + ".xml");
    string ansall = "";
    foreach (XElement mm in root.Elements("msginfo"))
    {
        if (msg == mm.Element("msg").Value)
        {
            ansall = mm.Element("ans").Value;
            break;
        }
    }
    return ansall;
}
public static void del(long group, string msg)
{
    dircheck(group);
    string gg = group.ToString();
    XElement root = XElement.Load(path + group + ".xml");
    var element = from ee in root.Elements()
                  where (string)ee.Element("msg") == msg
                  select ee;
    if (element.Count() > 0)
    {
        element.First().Remove();
    }
    root.Save(path + group + ".xml");
}
public static void insert(long group, string msg, string ans)
{
    if(msg.IndexOf("\r\n") < 0 & msg != "")
    {
        dircheck(group);
        XElement root = XElement.Load(path + group + ".xml");
        XElement read = root.Element("msginfo");
        read.AddBeforeSelf(new XElement("msginfo",
               //new XElement("group", group),
               new XElement("msg", msg),
               new XElement("ans", ans)
               ));
        root.Save(path + group + ".xml");
    }
}
public static void createxml(long group)
{
    XElement root = new XElement("Categories",
        new XElement("msginfo",
            //new XElement("group", 123),
            new XElement("msg", "初始问题"),
            new XElement("ans", "初始回答")
            )
       );
    root.Save(path + group + ".xml");
}
public static void dircheck(long group)
{
    if (File.Exists(path + group + ".xml"))
    {
        //MessageBox.Show("存在文件");
        //File.Delete(dddd);//删除该文件
    }
    else
    {
        //MessageBox.Show("不存在文件");
        createxml(group);//创建该文件,如果路径文件夹不存在,则报错。
    }
}

另外,因为我们需要统计时间差,所以我使用了timestamp来记录时间(不会用datetime?,求大佬讲解)

        /// <summary>  
        /// DateTime时间格式转换为Unix时间戳格式  
        /// </summary>  
        /// <param name="time"> DateTime时间格式</param>  
        /// <returns>Unix时间戳格式</returns>  
        public static int ConvertDateTimeInt(System.DateTime time)
        {
            System.DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1));
            return (int)(time - startTime).TotalSeconds;
        }

于是,整段响应群消息的代码如下:

/// <summary>
/// Type=2 群消息。
/// </summary>
/// <param name="subType">子类型,目前固定为1。</param>
/// <param name="sendTime">发送时间(时间戳)。</param>
/// <param name="fromGroup">来源群号。</param>
/// <param name="fromQQ">来源QQ。</param>
/// <param name="fromAnonymous">来源匿名者。</param>
/// <param name="msg">消息内容。</param>
/// <param name="font">字体。</param>
public override void GroupMessage(int subType, int sendTime, long fromGroup, long fromQQ, string fromAnonymous, string msg, int font)
{
    if (fromQQ == 80000000)
        return;
    // 处理群消息。
...一堆代码
    else if(msg.IndexOf("pixel") == 0 && (msg.Length - msg.Replace("/","").Length) == 2)
    {
        string fromqqtime_get = xml_get(20, fromQQ.ToString());
        int fromqqtime = 0;
        try
        {
            fromqqtime = int.Parse(fromqqtime_get);
        }
        catch { }
        if(ConvertDateTimeInt(DateTime.Now)-fromqqtime < 300)
        {
            SendMinecraftMessage(fromGroup, CQ.CQCode_At(fromQQ) + "你需要再等" + (300 - ConvertDateTimeInt(DateTime.Now) + fromqqtime) + "秒才能继续放像素点");
            return;
        }
        string get_msg = msg.Replace("pixel", ""), getx = "", gety = "", getcolor = "";
        int placex, placey;
        string[] str2;
        int count_temp = 0;
        str2 = get_msg.Split('/');
        foreach (string i in str2)
        {
            if (count_temp == 0)
            {
                getx = i.ToString();
                count_temp++;
            }
            else if (count_temp == 1)
            {
                gety = i.ToString();
                count_temp++;
            }
            else if (count_temp == 2)
            {
                getcolor = i.ToString();
                count_temp++;
            }
        }
        try
        {
            placex = int.Parse(getx) - 1;
            placey = int.Parse(gety) - 1;
            if (getcolor.IndexOf("#") == -1 || placex > 29 || placey > 29)
                throw new ArgumentNullException("fuck wrong color");
        }
        catch
        {
            SendMinecraftMessage(fromGroup, "放置像素点时遇到未知错误,请检查颜色与坐标是否正确");
            return;
        }
        int picCount;
        try
        {
            picCount = int.Parse(xml_get(20, "count"));
        }
        catch
        {
            SendMinecraftMessage(fromGroup, "遇到致命性错误,请联系晨旭修复");
            return;
        }
        try
        {
            string picPath = @"C:\Users\Administrator\Desktop\kuqpro\data\image\pixel_game\" + picCount + ".bmp";
            Bitmap pic = ReadImageFile(picPath);
            pic = SetPoint(pic, ColorTranslator.FromHtml(getcolor), placex, placey);
            picCount++;
            picPath = @"C:\Users\Administrator\Desktop\kuqpro\data\image\pixel_game\" + picCount + ".bmp";
            pic.Save(picPath);
        }
        catch
        {
            SendMinecraftMessage(fromGroup, "遭遇未知错误");
            return;
        }
        del(20, "count");
        del(20, fromQQ.ToString());
        insert(20, "count", picCount.ToString());
        insert(20, fromQQ.ToString(), ConvertDateTimeInt(DateTime.Now).ToString());
        SendMinecraftMessage(fromGroup, "[CQ:image,file=pixel_game\\" + picCount + ".bmp]\r\n图片修改完成!" + DateTime.Now.ToString() + CQ.CQCode_At(fromQQ));
    }
...一堆代码
}

如有问题,可以在下面留言。

代码写的比较啰嗦,大佬们请耐心地看?

树莓派实现24小时直播点歌功能

在这篇文章发布之前,我在it水家已经投过一稿了:http://www.ithome.com/html/win10/311694.htm

另外上一篇不带点歌功能,只有推流功能的教程:https://www.chenxublog.com/2017/06/02/raspi-live-24h-bilibili.html

如果是零基础,建议先看一遍上面那两篇文章,如果是使用代码,请参考本文。这几天代码已经更新多次。

我写的代码只能保证可用,毕竟我是业余的233333欢迎大神拿我的思路进行优化

github:https://github.com/chenxuuu/24h-raspberry-live-on-bilibili

php端运行的代码(最后更新于2017.6.4晚):

<head>
<meta charset="utf-8">
</head>
<body>
<form action="" method="post">
<p>一个简陋的点歌台23333</p>
<p>搜索结果来自网易云</p>
<p>输入歌曲名搜索歌曲:<input type="text" name="song" /></p>
<p><input type="submit" name="sub" value="搜索" /></p>
</form>
<form action="" method="post">
<p>或者直接输入id点歌(推荐!)<br/>实例(红色部分为id):<br/>http://music.163.com/song/<font color="red">26489014</font>/?userid=261620056<br/>http://music.163.com/#/song?id=<font color="red">32477053</font></p>
<p>id:<input type="text" name="id" /></p>
<p><input type="submit" name="sub" value="查看" /></p>
</form>
by 晨旭/chenxublog.com | running on Raspberry Pi 2 Model B<br/>直播间地址:http://live.bilibili.com/16703<br/><br/><br/>
当前状态:<div id='t'></div>
<?php
require_once 'NeteaseMusicAPI_mini.php';

function get_url_id($id)
{
    $api = new NeteaseMusicAPI();
    $result = $api->url($id);
    $result = str_replace("[","",$result);
    $result = str_replace("]","",$result);
    $data=json_decode($result, true);
    return $data['data']['url'];
}
function urlcheck($url)
{
    $counttemp=0;
    for($counttemp=1;$counttemp<31;$counttemp++)
    {
        if (file_exists($counttemp.'.txt') && $url==file_get_contents($counttemp.'.txt'))   
        {   
            return false;
        }
    }
    return true;
}
function netease_http($url)
{
    $refer = "http://music.163.com/";
    $header[] = "Cookie: " . "appver=1.5.0.75771;";
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
    curl_setopt($ch, CURLOPT_REFERER, $refer);
    $cexecute = curl_exec($ch);
    curl_close($ch);
    if ($cexecute)
    {
        $result = json_decode($cexecute, true);
        return $result;
    }
    else
    {
        return false;
    }
}
function getStatus($url){
    if($headers = @get_headers($url)){
            $status  = $headers[0];
            $statusno= false;
            if(preg_match_all('%HTTP/1\.1 ([\d]{3})%i',$status,$matches)){
                    $statusno = $matches[1][0];
            }
            return $statusno;
    }
        return false;
}

if(!empty($_POST['song'])){

$song=$_POST['song'];
echo '搜索关键词:'.$song.'<br/>=========================================================<br/>';
$url = "http://s.music.163.com/search/get/?type=1&s=".$song;
$response = netease_http($url);
$counttemp=0;
for($counttemp=0;$counttemp<10;$counttemp++)
{
    if(getStatus($response['result']['songs'][$counttemp]['audio'])=='200')
    {
        echo $response['result']['songs'][$counttemp]['name'].'<img src="'.$response['result']['songs'][$counttemp]['album']['picUrl'].'" height="100"/><a href="?down='.$response['result']['songs'][$counttemp]['audio'].'">就选这首了</a><br/>试听:<audio src="'.$response['result']['songs'][$counttemp]['audio'].'" controls="controls"></audio><br/>=========================================================<br/>';
    }
    else
    {
        echo '第'.($counttemp+1).'首歌曲获取失败(歌曲失效/版权问题/网络抽风)<br/>=========================================================<br/>';
    }
}

}
elseif(!empty($_POST['id'])){
    echo '试听:<audio src="'.get_url_id($_POST['id']).'" controls="controls"></audio><br/><a href="?down='.get_url_id($_POST['id']).'">就选这首了</a>';
}
elseif(!empty($_GET['down']) && empty($_GET['write']) && strpos($_GET['down'],"music.126.net")!=false && getStatus($_GET['down'])=='200')
{
    echo '当前可供选择的空闲的序列(注意,直播是从第1首到第30首按顺序播放的。歌曲长度最大限制六分钟,超过将丢弃。):<br/>';
    $counttemp=0;
    for($counttemp=1;$counttemp<31;$counttemp++)
    {
        if($counttemp%5==0)
        {
            echo '<br/>';
        }
        if (!file_exists($counttemp.'.txt'))
        {
            echo '<a href="?write='.$counttemp.'&down='.$_GET['down'].'">换掉第'.$counttemp.'首歌</a>(<font color="green">可以换歌</font>)|';
        }
        else
        {
            echo '第'.$counttemp.'首歌(<font color="red">排队渲染中</font>)|';
        }
    }
}
elseif(!empty($_GET['write']) && !empty($_GET['down']) && urlcheck($_GET['down']) && getStatus($_GET['down'])=='200')
{
    if(!file_exists($_GET['write'].'.txt'))
    {
        //file_put_contents('songs/'.$_GET['write'].'.txt', $_GET['down']);
        $myfile = fopen($_GET['write'].'.txt', "w") or die("Unable to open file!");
        fwrite($myfile, $_GET['down']);
        fclose($myfile);
        echo '第'.$_GET['write'].'首歌曲的渲染请求提交成功!<a href="index.php">返回首页</a>';
    }
    else
    {
        echo '啊哦!这个序号已经有人选中了!正在渲染!';
    }
}
else
{
    echo '树莓当前负载状态(注意,直播是从第1首到第30首按顺序播放的):<br/>';
    $counttemp=0;
    for($counttemp=1;$counttemp<31;$counttemp++)
    {
        if($counttemp%5==0)
        {
            echo '<br/>';
        }
        if (!file_exists($counttemp.'.txt'))
        {
            echo '第'.$counttemp.'首歌(<font color="green">可以换掉</font>)|';
        }
        else
        {
            echo '第'.$counttemp.'首歌(<font color="red">排队渲染中</font>)|';
        }
    }
}

?>
<script type="text/javascript" src="now.js"></script>
</body>

同目录下需要新建一个NeteaseMusicAPI_mini.php,文件取自:

https://github.com/metowolf/NeteaseCloudMusicApi/blob/master/weapi/NeteaseMusicAPI_mini.php

php文件我全部放到了/usr/share/nginx/www/songs目录下。

渲染处理部分:

ff.py(路径/home/pi/songs/)

# -*- coding:utf-8 -*-
import os
import urllib
import eyed3
import time
for i in range(1, 30+1):
  if(os.path.exists(str(i)+'.mp3')):
    os.remove(str(i)+'.mp3')
    os.remove('/usr/share/nginx/www/songs/'+str(i)+'.txt')
  if(os.path.exists('/usr/share/nginx/www/songs/'+str(i)+'.txt')):
    f = open('/usr/share/nginx/www/songs/'+str(i)+'.txt')
    content = f.read()
    fileout = file('/usr/share/nginx/www/songs/now.js','w')
    fileout.write('t.innerHTML=("正在下载'+str(i)+'.mp3")')
    fileout.close()
    urllib.urlretrieve(content, str(i)+'.mp3')
    print('download success')
    xx=eyed3.load(str(i)+'.mp3')
    seconds=xx.info.time_secs
    if(seconds<600):
      fileout = file('/usr/share/nginx/www/songs/now.js','w')
      fileout.write('t.innerHTML=("正在生成'+str(i)+'.mp4的一图流视频 第一步/共两步")')
      fileout.close()
      os.system('ffmpeg -loop 1 -r 1 -t '+str(seconds)+' -f image2 -i '+str(i)+'.png -vcodec libx264 -pix_fmt yuv420p -crf 24 -y SinglePictureVideo.mp4')
      fileout = file('/usr/share/nginx/www/songs/now.js','w')
      fileout.write('t.innerHTML=("正在将'+str(i)+'.flv的视频与音频合为一体 第二步/共两步")')
      fileout.close()
      os.system('ffmpeg -i SinglePictureVideo.mp4 -i '+str(i)+'.mp3 -c:v copy -c:a aac -y '+str(i)+'.flv')
      os.remove(str(i)+'.mp3')
      os.remove('SinglePictureVideo.mp4')
      os.remove('/usr/share/nginx/www/songs/'+str(i)+'.txt')
      fileout = file('/usr/share/nginx/www/songs/now.js','w')
      fileout.write('t.innerHTML=("成功渲染'+str(i)+'.flv!60秒后会开始渲染下一个视频")')
      fileout.close()
    else:
      os.remove(str(i)+'.mp3')
      os.remove('/usr/share/nginx/www/songs/'+str(i)+'.txt')
    time.sleep(60)
time.sleep(10)

ff.sh(路径/home/pi/songs/)

#!/bin/bash
while true
do
 python ff.py
done

路径/home/pi/songs/下还有1-30.png文件用于作为视频内容

启动ffmpeg部分:

playlist.txt请自己写吧,规则见https://trac.ffmpeg.org/wiki/Concatenate

live.sh(与playlist.txt同级)

#!/bin/bash
while true
do
  ffmpeg -re -f concat -safe 0 -i playlist.txt -vcodec copy -acodec aac -b:a 192k -f flv "你的直播地址和码"
done

理论上到这里就结束了,但是网络老断,那我只能暴力检测和强制重启推流来解决了

断网自动重推部分:

net.py

#coding:utf8
'''python3 code
author's email: chenyan@feling.net
通过统计ifconfig命令的输出,计算当前网速
'''

import logging
logging.basicConfig(level=logging.INFO,
                format='%(message)s',
                #filename='speed',
                #filemod='w'
                )

import os, sys, time
import re

def get_total_tx_bytes(interface, isCN):
    grep = '发送字节' if isCN else '"TX bytes"'
    r = os.popen('ifconfig '+interface+' | grep '+grep).read()
    total_bytes = re.sub('(.+:)| \(.+','',r)
    return int(total_bytes)

def get_total_rx_bytes(interface, isCN):
    grep = '接收字节' if isCN else '"RX bytes"'
    r = os.popen('ifconfig '+interface+' | grep '+grep).read()
    total_bytes = re.sub(' \(.+','',r)
    total_bytes = re.sub('.+?:','',total_bytes)
    return int(total_bytes)


if __name__=='__main__':
    interface = sys.argv[1]
    get_total_bytes = get_total_tx_bytes if sys.argv[2]=='tx' else get_total_rx_bytes
    isCN = True if sys.argv[3]=='cn' else False
    freq = int(sys.argv[4])
    low_count = 0
    for i in range(1, 10 + 1):
        last = get_total_bytes(interface, isCN)
        time.sleep(freq)
        increase = get_total_bytes(interface, isCN) - last
        logging.info(str(increase/freq/1000))
        speed_now = increase/freq/1000
        if(speed_now < 5):
            low_count = low_count +1
    if(low_count > 5):
        os.system('killall ffmpeg')
        time.sleep(1)
        os.system('killall ffmpeg')
        time.sleep(1)
        os.system('killall ffmpeg')
        logging.info('666')
    else:
        logging.info('ok!')

net.sh

#!/bin/bash
while true
do
python net.py eth0 tx false 1
done

这样所有我用的脚本都贴在这里了,启动方式就是扔几个screen就行了

screen sh live.sh
#按ctrl+a,ctrl+d
screen sh net.sh
#按ctrl+a,ctrl+d
cd songs/
screen sh ff.sh
#按ctrl+a,ctrl+d

完毕~

技术不高,如有错误,请指出,谢谢o(* ̄▽ ̄*)ブ

利用树莓派实现b站24小时音乐直播

本教程参考了以下文章:

ffmpeg-rtmp搞事指南

树莓派安装ffmpeg

使用FFmpeg在B站直播的姿势

第一步:安装ffmpeg

先安装解码器

git clone git://git.videolan.org/x264
cd x264
./configure --host=arm-unknown-linux-gnueabi --enable-static --disable-opencl
make
sudo make install
cd ..
rm -rf x264

再安装ffmpeg

git clone git://source.ffmpeg.org/ffmpeg.git
cd ffmpeg
sudo ./configure --arch=armel --target-os=linux --enable-gpl --enable-libx264 --enable-nonfree
make
sudo make install
cd ..
rm -rf ffmpeg

有两个地方需要注意:

  1. git下载慢的话可以先导入到gitosc再clone

  2. 编译ffmpeg的过程在树莓上十分漫长。。。我的花了两个半小时,所以建议使用screen扔到后台,以免断网导致需要重来

第二步:准备直播所需要的视频文件

我们需要制作一个只有背景音乐的视频,一般会做一个一图流的视频(整个视频画面就是一张图)

这里我使用了小丸工具箱(点我下载

先要合并一堆mp3文件,具体方法:

  1. 全选这些mp3文件

  2. 右击添加到压缩文件

  3. 文件类型选择zip,压缩类型选择“储存”

  4. 将压缩好的xxx.zip拓展名改为.mp3即可(是不是很神奇hhhhh)

接下来照一张图片,打开小丸工具箱,选择“常用”选项卡

把图片拖到“一图流”的“图片”框里,把整合好的音乐拖到“音频”框里,点击“压制”

这样我们就得到了我们需要的文件

第三步:进行推流

把文件传到树莓上,使用下面的命令即可推流

ffmpeg -re -i "1.mp4" -vcodec copy -acodec aac -b:a 192k -f flv "你的rtmp地址/你的直播码"

由于此命令只能运行一次,播放完就停了,所以要实现无限循环播放的话就需要编写一个sh文件,内容如下

#!/bin/bash
while true
do
ffmpeg -re -i "1.mp4" -vcodec copy -acodec aac -b:a 192k -f flv "你的rtmp地址/你的直播码"
done

保存为live.sh,使用下面的命令运行即可(如果没有screen,先使用sudo apt-get screen 安装)

screen nohup sh live.sh &

搭建好的直播间:http://live.bilibili.com/16703

不保证我以后的直播内容不变哈

外部中断实验

代码构造均取自野火教程,只是加上了一些注释。

按键和 EXTI 宏定义:

//引脚定义
#define KEY1_INT_GPIO_PORT GPIOA
//GPIOA
#define KEY1_INT_GPIO_CLK RCC_AHB1Periph_GPIOA
//打开AHB1时钟
#define KEY1_INT_GPIO_PIN GPIO_Pin_0
//pin0
#define KEY1_INT_EXTI_PORTSOURCE EXTI_PortSourceGPIOA
//GPIOA中断
#define KEY1_INT_EXTI_PINSOURCE EXTI_PinSource0
//第0引脚中断
#define KEY1_INT_EXTI_LINE EXTI_Line0
//选择EXTI中断源(PA0)
#define KEY1_INT_EXTI_IRQ EXTI0_IRQn
//IRQ通道

#define KEY1_IRQHandler EXTI0_IRQHandler
//中断服务函数

//以下同上
#define KEY2_INT_GPIO_PORT GPIOC
#define KEY2_INT_GPIO_CLK RCC_AHB1Periph_GPIOC
#define KEY2_INT_GPIO_PIN GPIO_Pin_13
#define KEY2_INT_EXTI_PORTSOURCE EXTI_PortSourceGPIOC
#define KEY2_INT_EXTI_PINSOURCE EXTI_PinSource13
#define KEY2_INT_EXTI_LINE EXTI_Line13
#define KEY2_INT_EXTI_IRQ EXTI15_10_IRQn

#define KEY2_IRQHandler EXTI15_10_IRQHandler

嵌套向量中断控制器 NVIC 配置:

 /**
  * @brief  配置嵌套向量中断控制器NVIC
  * @param  无
  * @retval 无
  */
static void NVIC_Configuration(void)
{
  NVIC_InitTypeDef NVIC_InitStructure;
  
  /* 配置NVIC为优先级组1 */
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
  
  /* 配置中断源:按键1 */
  NVIC_InitStructure.NVIC_IRQChannel = KEY1_INT_EXTI_IRQ;
  /* 配置抢占优先级:1 */
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
  /* 配置子优先级:1 */
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
  /* 使能中断通道 */
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
  
  /* 配置中断源:按键2,其他使用上面相关配置 */  
  NVIC_InitStructure.NVIC_IRQChannel = KEY2_INT_EXTI_IRQ;
  NVIC_Init(&NVIC_InitStructure);
}


EXTI 中断配置:

 /**
  * @brief  配置 PA0 为线中断口,并设置中断优先级
  * @param  无
  * @retval 无
  */
void EXTI_Key_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure; 
EXTI_InitTypeDef EXTI_InitStructure;
  
/*开启按键GPIO口的时钟*/
RCC_AHB1PeriphClockCmd(KEY1_INT_GPIO_CLK|KEY2_INT_GPIO_CLK ,ENABLE);
  
  /* 使能 SYSCFG 时钟 ,使用GPIO外部中断时必须使能SYSCFG时钟*/
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
  
  /* 配置 NVIC */
  NVIC_Configuration();
  
/* 选择按键1的引脚 */ 
  GPIO_InitStructure.GPIO_Pin = KEY1_INT_GPIO_PIN;
  /* 设置引脚为输入模式 */ 
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;    
  /* 设置引脚不上拉也不下拉 */
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
  /* 使用上面的结构体初始化按键 */
  GPIO_Init(KEY1_INT_GPIO_PORT, &GPIO_InitStructure); 
/* 连接 EXTI 中断源 到key1引脚 */
  SYSCFG_EXTILineConfig(KEY1_INT_EXTI_PORTSOURCE,KEY1_INT_EXTI_PINSOURCE);
  /* 选择 EXTI 中断源 */
  EXTI_InitStructure.EXTI_Line = KEY1_INT_EXTI_LINE;
  /* 中断模式 */
  EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
  /* 下降沿触发 */
  EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;  
  /* 使能中断/事件线 */
  EXTI_InitStructure.EXTI_LineCmd = ENABLE;
  EXTI_Init(&EXTI_InitStructure);
  
  /* 选择按键2的引脚 */ 
  GPIO_InitStructure.GPIO_Pin = KEY2_INT_GPIO_PIN;  
  /* 其他配置与上面相同 */
  GPIO_Init(KEY2_INT_GPIO_PORT, &GPIO_InitStructure);      
/* 连接 EXTI 中断源 到key2 引脚 */
  SYSCFG_EXTILineConfig(KEY2_INT_EXTI_PORTSOURCE,KEY2_INT_EXTI_PINSOURCE);
  /* 选择 EXTI 中断源 */
  EXTI_InitStructure.EXTI_Line = KEY2_INT_EXTI_LINE;
  EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
  /* 上升沿触发 */
  EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;  
  EXTI_InitStructure.EXTI_LineCmd = ENABLE;
  EXTI_Init(&EXTI_InitStructure);
}

EXTI 中断服务函数(一般会把中断函数放入stm32f4xx_it.c文件当中):

void KEY1_IRQHandler(void)
{
  //确保是否产生了EXTI Line中断
	if(EXTI_GetITStatus(KEY1_INT_EXTI_LINE) != RESET) 
	{
		// LED1 取反		
		LED1_TOGGLE;
    //清除中断标志位
		EXTI_ClearITPendingBit(KEY1_INT_EXTI_LINE);     
	}  
}

void KEY2_IRQHandler(void)
{
  //确保是否产生了EXTI Line中断
	if(EXTI_GetITStatus(KEY2_INT_EXTI_LINE) != RESET) 
	{
		// LED2 取反		
		LED2_TOGGLE;
    //清除中断标志位
		EXTI_ClearITPendingBit(KEY2_INT_EXTI_LINE);     
	}  
}

理解值和引用-第八章-C#从入门到放弃

Circle c;
c = new Circle(42);
Circle refc;
refc = c;  //c与refc指向同一地址

正确的复制类的方法(包括复制私有数据):

class Circle
{
    private int radius;
    ...
    public Circle Clone()
    {
        //创建新的Circle对象
        Circle clone = new Circle();
        //将私有的数据从this复制到clone
        clone.radius = this.radius;
        
        //返回包含克隆数据的新Circle对象
        return clone;
    }
}

null值:

Circle c = new Circle(42);
Circle copy = null;
...
if(copy == null)
{
    copy = c;
    ...
}

对于值类型,需要将变量生命为可空值类型:

int i = null; //非法
int? i = null; //合法

可空类型的属性:

int? i = null;
...
if(!i.HasValus)
{
    i = 99;
}
else
{
    Console.WriteLine(i.Value);
}

ref:

static void example(ref int a)
{
    a++;
}
static void Main()
{
    int arg = 42;
    example(ref arg);
    //arg=43
}

out:

static void example(out int a)
{
    a = 42;
}
static void Main()
{
    int arg;
    example(out arg);
    //arg=42
}

object:

Circle c;
c = new Circle(42);
object o;
o = c;

创建和管理类和对象 – 第七章 – C# 从入门到放弃

创建构造器的一个例子:

class Circle
{
    private int radius;
    
    public Circle()  //默认构造器
    {
        radius = 0;
    }
    public Circle(int initalRadius)  //重载的构造器
    {
        radius = initialRadius;
    }
    public double Area()
    {
        return Math.PI * radius * radius;
    }
}

使用多文件时的写法:

Circ1.cs内容:

partial class Circle
{
    public Circle()  //默认构造器
    {
        radius = 0;
    }
    public Circle(int initalRadius)  //重载的构造器
    {
        radius = initialRadius;
    }
}

Circ2.cs内容:

partial class Circle
{
    private int radius;
    public double Area()
    {
        return Math.PI * radius * radius;
    }
}

调用自己的字段时,使用this关键字:

class Point
{
    private int x, y;
    private static int objectCount = 0;
    public Point()
    {
        this.x = -1;
        this.y = -1;
        objectCount++;
    }
    public Point(int x, int y)
    {
        this.x = x;
        this.y = y;
        objectCount++;
    }
    public double DistanceTo(Point other)
    {
        int xDiff = this.x - other.x;   //注意此处
        int yDiff = this.y - other.y;
        double distance = Math.Sqrt((xDiff * xDiff) + (yDiff * yDiff));
        return distance;
    }
    public static int ObjectCount() => objectCount;
    //ObjectCount方法返回objectCount的值
}

=======

静态方法/数据使用static标记

只能访问标记为static的其他方法和字段

静态类(不允许被new:

class Math
{
    ...
    public const double PI = 3.14159265358979;
}

匿名类:

var myxxxx = new {Name = "xx", Age = 123};
Console.WriteLine($"Name:{myxxxx.Name},Age:{myxxxx.Age}");


var myyyyy = new {Name = "yy", Age = 321};

myxxxx = myyyyy; //合法,因为具有相同类型