2024年2月

前言

Table表格在后台管理应用中使用的是相当频繁的,因此找一个功能齐全的前端框架对于我们而言是非常必要的,因为封装完善的前端框架能够大大提升我们的工作对接效率。今天我们主要来讲解一下在.NET中使用BootstrapBlazor组件库的Table表格组件(
本章使用的数据都是程序自动生成的模拟数据,不需要与数据库打交道
)。

BootstrapBlazor介绍

BootstrapBlazor是一套基于 Bootstrap 和 Blazor 的企业级组件库,可以认为是 Bootstrap 项目的 Blazor 版实现。基于 Bootstrap 样式库精心打造,并且额外增加了 100 多种常用的组件,为您快速开发项目带来非一般的感觉(喜欢Bootstrap风格的同学推荐使用)。

.NET BootstrapBlazor UI组件库引入

BootstrapBlazor Table使用前提条件:
https://mp.weixin.qq.com/s/UIeKSqym8ibLRvDwra8aww

首先定义StudentViewModel

    public class StudentViewModel
    {
        /// <summary>
        /// StudentID
        /// </summary>
        public int StudentID { get; set; }

        /// <summary>
        /// 班级名称
        /// </summary>
        public string ClassName { get; set; }

        /// <summary>
        /// 学生姓名
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 学生年龄
        /// </summary>
        public int Age { get; set; }

        /// <summary>
        /// 学生性别
        /// </summary>
        public string Gender { get; set; }
    }

.NET后台模拟数据和增删改查方法封装

using BootstrapBlazor.Components;
using WebUI.Model;

namespace WebUI.Pages
{
    public partial class StudentExample
    {
        private static readonly Random random = new Random();
        public static List<StudentViewModel>? StudentInfoList;

        public StudentExample()
        {
            StudentInfoList = GenerateUserInfos();
        }

        /// <summary>
        /// 模拟数据库用户信息生成
        /// </summary>
        /// <returns></returns>
        public static List<StudentViewModel> GenerateUserInfos()
        {
            return new List<StudentViewModel>(Enumerable.Range(1, 200).Select(i => new StudentViewModel()
            {
                StudentID = i,
                ClassName = $"时光 {i} 班",
                Name = GenerateRandomName(),
                Age = random.Next(20, 50),
                Gender = GenerateRandomGender()
            }));
        }

        /// <summary>
        /// 生成随机性别
        /// </summary>
        /// <returns></returns>
        public static string GenerateRandomGender()
        {
            string[] genders = { "男", "女" };
            return genders[random.Next(genders.Length)];
        }

        /// <summary>
        /// 生成随机姓名
        /// </summary>
        /// <returns></returns>
        public static string GenerateRandomName()
        {
            string[] surnames = { "张", "王", "李", "赵", "刘" };
            string[] names = { "明", "红", "强", "丽", "军" };
            string surname = surnames[random.Next(surnames.Length)];
            string name = names[random.Next(names.Length)];
            return surname + name;
        }

        /// <summary>
        /// 数据查询
        /// </summary>
        /// <param name="options">options</param>
        /// <returns></returns>
        private Task<QueryData<StudentViewModel>> OnQueryAsync(QueryPageOptions options)
        {
            List<StudentViewModel> studentInfoData = StudentInfoList;

            // 数据模糊过滤筛选
            if (!string.IsNullOrWhiteSpace(options.SearchText))
            {
                studentInfoData = studentInfoData.Where(x => x.Name.Contains(options.SearchText)).ToList();
            }

            return Task.FromResult(new QueryData<StudentViewModel>()
            {
                Items = studentInfoData.Skip((options.PageIndex - 1) * options.PageItems).Take(options.PageItems).ToList(),
                TotalCount = studentInfoData.Count()
            });
        }

        /// <summary>
        /// 模拟数据增加和修改操作
        /// </summary>
        /// <param name="studentInfo">studentInfo</param>
        /// <param name="changedType">changedType</param>
        /// <returns></returns>
        public Task<bool> OnSaveAsync(StudentViewModel studentInfo, ItemChangedType changedType)
        {
            if (changedType.ToString() == "Update")
            {
                var queryInfo = StudentInfoList.FirstOrDefault(x => x.StudentID == studentInfo.StudentID);
                if (queryInfo != null)
                {
                    queryInfo.Age = studentInfo.Age;
                    queryInfo.ClassName = studentInfo.ClassName;
                    queryInfo.Name = studentInfo.Name;
                    queryInfo.Gender = studentInfo.Gender;
                }
            }
            else if (changedType.ToString() == "Add")
            {
                StudentInfoList.Add(studentInfo);
            }
            return Task.FromResult(true);
        }

        /// <summary>
        /// 数据删除
        /// </summary>
        /// <param name="items">items</param>
        /// <returns></returns>
        private Task<bool> OnDeleteAsync(IEnumerable<StudentViewModel> items)
        {
            items.ToList().ForEach(i => StudentInfoList.Remove(i));
            return Task.FromResult(true);
        }
    }
}

一行代码快速生成Table表格

<Table TItem="StudentViewModel" AutoGenerateColumns="true" Items="StudentInfoList"></Table>

显示Table工具栏

<Table TItem="StudentViewModel" AutoGenerateColumns="true" Items="StudentInfoList" ShowToolbar="true"></Table>

显示Table多选模式

<Table TItem="StudentViewModel" AutoGenerateColumns="true" Items="StudentInfoList" ShowToolbar="true" IsMu

增加Table搜索功能

<Table TItem="StudentViewModel" AutoGenerateColumns="true" Items="StudentInfoList" ShowToolbar="true" IsMultipleSelect="true" ShowSearch="true">

    <SearchTemplate>
        <GroupBox Title="搜索条件">
            <div class="row g-3 form-inline">
                <div class="col-12 col-sm-6">
                    <BootstrapInput @bind-Value="@context.Name" PlaceHolder="请输入姓名" maxlength="50" ShowLabel="true" DisplayText="姓名" />
                </div>
                <div class="col-12 col-sm-6">
                    <BootstrapInput @bind-Value="@context.Gender" PlaceHolder="请输入性别" maxlength="500" ShowLabel="true" DisplayText="性别" />
                </div>
            </div>
        </GroupBox>
    </SearchTemplate>
</Table>

增加Table增、删、改、查、分页功能

<Table TItem="StudentViewModel"
       AutoGenerateColumns="true"
       ShowToolbar="true"
       IsMultipleSelect="true"
       OnSaveAsync="@OnSaveAsync"
       OnQueryAsync="@OnQueryAsync"
       OnDeleteAsync="@OnDeleteAsync"
       IsStriped="true"
       IsBordered="true"
       ShowSearch="true"
       IsPagination="true"
       ShowSearchText="true">

    <TableColumns>
        <TableColumn Sortable="true" Filterable="true" Searchable="true" @bind-Field="@context.StudentID" />
        <TableColumn Sortable="true" Filterable="true" Searchable="true" @bind-Field="@context.Name" />
        <TableColumn Sortable="true" Filterable="true" Searchable="true" @bind-Field="@context.ClassName" />
        <TableColumn Sortable="true" Filterable="true" Searchable="true" @bind-Field="@context.Gender" />
    </TableColumns>

    <SearchTemplate>
        <GroupBox Title="搜索条件">
            <div class="row g-3 form-inline">
                <div class="col-12 col-sm-6">
                    <BootstrapInput @bind-Value="@context.Name" PlaceHolder="请输入姓名" maxlength="50" ShowLabel="true" DisplayText="姓名" />
                </div>
                <div class="col-12 col-sm-6">
                    <BootstrapInput @bind-Value="@context.Gender" PlaceHolder="请输入性别" maxlength="500" ShowLabel="true" DisplayText="性别" />
                </div>
            </div>
        </GroupBox>
    </SearchTemplate>
</Table>

DotNetGuide技术社区交流群

  • DotNetGuide技术社区是一个面向.NET开发者的开源技术社区,旨在为开发者们提供全面的C#/.NET/.NET Core相关学习资料、技术分享和咨询、项目推荐、招聘资讯和解决问题的平台。
  • 在这个社区中,开发者们可以分享自己的技术文章、项目经验、遇到的疑难技术问题以及解决方案,并且还有机会结识志同道合的开发者。
  • 我们致力于构建一个积极向上、和谐友善的.NET技术交流平台,为广大.NET开发者带来更多的价值和成长机会。





欢迎加入DotNetGuide技术社区微信交流群

大家都忙一年了,所以今天来点轻松的吧!就是那种拿来直接用、免费看的开源项目。

开源真是一个充满惊喜的宝库,很多开源软件比收费软件还好用,比如这款开箱即用的电视直播软件:my-tv,它免费、无广告、启动快,内置高质量直播源,主打一个免费好用。我最近被云厂商的一键购买「幻兽帕鲁」私服刷屏了,不想买云服务怎么办?这有个可实现一键本地部署「幻兽帕鲁」的 Docker 项目,看到 Docker 字样我就不用多说啥了,此容器经测试可正常运行于 Ubuntu/Debian、Windows10、macOS。

还有点开就能体验文本转化语音,仅需 1 分钟的样本就能提升声音相似度和真实感的项目。最后是一个提交网站到 Google 搜索的脚本,也是那种配置一下就能跑的实用小工具。

没点干货吗?双手奉上「现代 C++ 编程课程」和「动手构建大语言模型」一书,开卷有益,路上无聊可以看看。

  • 本文目录
    • 1. GitHub 热搜项目
      • 1.1 开箱即用的电视直播软件:my-tv
      • 1.2 幻兽帕鲁 Docker:palworld-server-docker
      • 1.3 网站编入 Google 的工具:google-indexing-script
      • 1.4 少样本语音转换和合成工具:GPT-SoVITS
      • 1.5 动手构建大语言模型:LLMs-from-scratch
    • 2. HelloGitHub 热评
      • 2.1 现代 C++ 编程课程:Modern-CPP-Programming
      • 2.2 一款清爽的轻量级备忘录中心:memos
    • 3. 往期回顾

1. GitHub 热搜项目

1.1 开箱即用的电视直播软件:my-tv

主语言:C

Star:10k

周增长:6.9k

这是一款开源、免费、无广告、不用注册的电视直播软件,适用于 Android 5 及以上的手机和电视盒子。它安装即用、启动快,没有花里胡哨的 UI 和弹框,内置中央台、地方台等优质直播源,画质高清、播放流畅,

GitHub 地址→
https://github.com/lizongying/my-tv

1.2 幻兽帕鲁 Docker:palworld-server-docker

主语言:Shell

Star:1.9k

周增长:500

该项目是用于构建「幻兽帕鲁」专用服务器的 Docker 容器,服务器最低配置要求 CPU 4 核、内存 16 GB、存储空间 4 GB,可正常运行于 Ubuntu/Debian、 Windows 10 、macOS 操作系统,微调示例配置文件后,可实现 Docker 一键启动服务。

docker run -d \
    --name palworld-server \
    -p 8211:8211/udp \
    -p 27015:27015/udp \
    -v ./<palworld-folder>:/palworld/ \
    --env-file .env \
    --restart unless-stopped \
    --stop-timeout 30 \
    thijsvanloef/palworld-server-docker:latest

GitHub 地址→
https://github.com/thijsvanloef/palworld-server-docker

1.3 网站编入 Google 的工具:google-indexing-script

主语言:JavaScript

Star:5.2k

周增长:1.4k

该项目是一个简单的脚本,免去了一个个提交连接的繁琐,可以批量地将你的整个网站提交到 Google 索引,收录成功后预计需要 24-48 小时才会在 Google 上展示。

GitHub 地址→
https://github.com/goenning/google-indexing-script

1.4 少样本语音转换和合成工具:GPT-SoVITS

主语言:Python

Star:9.1k

周增长:2.7k

强大的少样本语音转换和语音合成 WebUI 工具,输入 5 秒的声音样本就能体验文本到语音转换。支持少样本 TTS、英语、日语和中文,集成了声音伴奏分离、中文自动语音识别和文本标注等功能。

GitHub 地址→
https://github.com/RVC-Boss/GPT-SoVITS

1.5 动手构建大语言模型:LLMs-from-scratch

主语言:Jupyter Notebook

Star:8.3k

周增长:5.3k

这是一本讲述如何从头制作一个类似 ChatGPT 的大语言模型的书,它介绍了 LLMs 的工作原理,并教你如何创建自己的 LLM,内含丰富的图示和代码示例,目前该书还在编写中未完结。

GitHub 地址→
https://github.com/rasbt/LLMs-from-scratch

2. HelloGitHub 热评

在这个章节,将会分享下本周 HelloGitHub 网站上的热门开源项目,欢迎与我们分享你上手这些开源项目后的使用体验。

2.1 现代 C++ 编程课程:Modern-CPP-Programming

主语言:C++

该教程面向有一定 C++ 编程基础的人,内容涵盖 C++ 编程的基础知识、高级 C++ 语义和概念。它免费且持续更新,共 22 讲约 1500 张幻灯片,实践教学不冗长,用简短的描述和代码进行讲解,许多例子和问题都来自一线开发者的真实案例。

项目详情→
https://hellogithub.com/repository/ae2f44f9ed1746809bd8fd6b677a3fb4

2.2 一款清爽的轻量级备忘录中心:memos

主语言:Go

这是一个采用 React+Tailwind+TypeScript+Go 开发的备忘录中心,相当于极简的微博。支持私有/公开备忘录、标签、互动式日历等功能,以及 Docker 部署。

项目详情→
https://hellogithub.com/repository/98fb40421cff4d5881412b0af9b68164

3. 往期回顾

往期回顾:


以上为 2024 年第 6 个工作周的 GitHub Trending

wmproxy

wmproxy
已用
Rust
实现
http/https
代理,
socks5
代理, 反向代理, 负载均衡, 静态文件服务器,
websocket
代理,四层TCP/UDP转发,内网穿透等,会将实现过程分享出来,感兴趣的可以一起造个轮子

项目地址

国内: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

设计目标

设计高可用的自定义的官网,可在自定义的情况下又可以快速的创建好官网。在官网文档的同时可能会夹杂博客功能等。

平台选型

一开始选择的是博客平台,
VanBlog
,平台部署也非常的的简单。
仅仅运行一行脚本即可以完成安装:

curl -L https://vanblog.mereith.com/vanblog.sh -o vanblog.sh && chmod +x vanblog.sh && ./vanblog.sh

如果博客的内容也非常的丰富,功能也很强大,如果用来搭建个人的博客平台完全ok。
但是如果用来弄官网主页,博客类型的不太适合,还得重新做选择平台。

后续继续寻找合适平台:

ekyll:一个非常流行的静态网站生成器,特别适合用于个人博客。它与GitHub Pages紧密集成,可以轻松托管。许多现成的Jekyll主题包含文档页面。

Hugo:一个用Go语言编写的快速静态网站生成器。它有一个活跃的社区,提供大量的主题和模板,其中一些专为文档和博客设计。
Hexo:一个快速、简单且强大的博客框架,使用Node.js编写。Hexo拥有丰富的插件和主题,易于集成文档页面。
Docusaurus:由Facebook维护,专为文档网站设计,但也可以用作博客。Docusaurus支持Markdown,易于撰写内容,并提供版本控制功能。
Gatsby:一个现代网站框架,使用React构建。Gatsby不仅适用于博客,还适用于更复杂的网站。很多Gatsby模板都包含了文档页面。
VuePress:VuePress是以Vue驱动的静态网站生成器,非常适合编写技术文档,并可以用于创建个人博客。
Pelican:使用Python编写的静态网站生成器,适用于博客和个人网站。支持Markdown和reStructuredText格式。
MkDocs:一个用Python编写的静态网站生成器,专注于项目文档创建,但也可以用于构建个人博客。

最终选型

最终在选择的时候选择了由vue开发的
VuePress
,且选择的是他的V2版本,但是目前V2还没有进入到非常的完整的阶段。

他的官方文档地址为
docs

通常会设置淘宝的国内源新地址

http://www.npmmirror.com/

此处我们不想用cnpm,仅仅只设置了代理

npm config set registry https://registry.npmmirror.com

这样子就可以在不改变npm命令的情况下拥有相当快的下载速度。

通过命令行创建示例项目

npm init vuepress vuepress-starter

通过
npm install
安装依赖项目

通过
run docs:dev
启动本地开发环境

npm run docs:dev

通过
run docs:build
启动本地打包,打包后的文件默认在
.vuepress/dist

npm run docs:build

添加mermaid支持

项目中因存在许多流程图文件,需添加mermaid支持,通过查询
插件市场
,寻找支持的插件发现
vuepress-plugin-md-enhance
支持v2版本的md扩展支持,安装依赖

npm i vuepress-plugin-md-enhance
npm i mermaid 

然后在配置中添加

import { mdEnhancePlugin } from "vuepress-plugin-md-enhance";

export default defineUserConfig({
  plugins: [
    mdEnhancePlugin({
      // 启用 mermaid
      mermaid: true,
    }),
  ],
};

即可启用mermaid支持,我们在任意的md中添加如下代码


flowchart LR
    我 -->|尝试| 使用wmproxy -->|简单易用| 喝杯咖啡
    

image.png

本地搜索功能

相对我们的文档相对简单,且数据量不会太大,可以用本地化的搜索插件来完成。这里我们选择
plugin-search-pro
来进行支持。
添加支持:

npm i -D plugin-search-pro

然后在配置里添加

import { searchProPlugin } from "vuepress-plugin-search-pro";
export default defineUserConfig({
  plugins: [
    searchProPlugin({
      customFields: [
        {
          name: "author",
          getter: (page) => page.frontmatter.author,
          formatter: "作者:$content",
        },
      ],
    }),
  ],
});

点右上角的搜索即可索引相关的路径:

image.png

添加统一的页眉页脚文件

我们可能在每个文章页统一引入前缀和后缀,那么我们不可能重复的书写相同的文字,此时我们需要用引入文件的形式来进行处理。此时我们用的也是md插件:

mdEnhancePlugin({
  // 启用导入支持
  include: true,
}),

我们就可以在任意的md文件中引入
<!-- @include: ./../common/footer.md -->
就可以导入该文件内容在我们的文章页插入了。方便重复内容的引用。

其它功能

其它功能如添加导航条,logo,显示,内容,多语言等可以参考官方文档。

打包发布

我们已经构建完文档的内容,我们开始着手打包项目。
我们使用
npm run docs:build
,运行完后就可以在
dist
目录下找到打包好的文件。
image.png
接下来我们需要一个文件服务系统在web进行部署即可完成官网的布置。

布置文件服务器

安装docker服务

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh --mirror Aliyun

利用
docker pull dreamwhat/wmproxy
获取镜像。

我们利用wmproxy服务来进行文件服务器,利用file-server的子命令

Usage: wmproxy.exe file-server [-r=ARG] [-l=ARG] [--listen-ssl=ARG] [--cert=ARG] [--key=ARG] [-d=ARG]
[-b] [--robots=ARG] [--path404=ARG] [-c=ARG] [-e=ARG]... [--cors] [-H=ARG]... [--access-log=ARG] [--control
=ARG] [--disable-stdout] [--disable-control] [--daemon] [--forever] [-v] [--default-level=ARG] [--pidfile
=ARG]

Available options:
    -r, --root=ARG           静态文件根目录路径
    -l, --listen=ARG         监听地址
                             [default: 127.0.0.1:8869]
        --listen-ssl=ARG     监听地址
        --cert=ARG           ssl证书cert
        --key=ARG            ssl证书key
    -d, --domain=ARG         域名地址
    -b, --browse             是否支持目录
        --robots=ARG         设置robots.txt返回
        --path404=ARG        设置404文件返回
    -c, --cache-time=ARG     设置robots.txt返回
    -e, --ext-mimetype=ARG   设置robots.txt返回
        --cors               通过"Access-Control-Allow-Origin"标头启用 CORS
    -H, --header=ARG         头部信息修改如 "proxy x-forward-for {client_ip}"
        --access-log=ARG     访问日志放的位置如"logs/access.log trace"
        --control=ARG        输入控制台的监听地址
                             [default: 127.0.0.1:8837]
        --disable-stdout     禁用默认输出
        --disable-control    禁用控制微端
        --daemon             后台运行
        --forever            守护程序运行,正常退出结束
    -v, --verbose            是否显示更多日志
        --default-level=ARG  设置默认等级
        --pidfile=ARG        写入进程id文件
    -h, --help               Prints help information

我们将同时支持http及https的支持,且我们将配置404出错时的文件,那么我们的命令将是:

wmproxy file-server -r /source/dist --listen 0.0.0.0:80 --listen-ssl 0.0.0.0:443 --path404 /source/dist/404.html --cert /source/key/wmproxy.net.pem --key /source/key/wmproxy.net.key

完整的docker-compose文件如下

version: '3.5'
services:

  wmproxy_client:
    container_name: wmproxy_client_docker        # 指定容器的名称
    image: dreamwhat/wmproxy:latest
    command:
      - sh
      - -c
      - |
        wmproxy file-server -r /source/dist --listen 0.0.0.0:80 --listen-ssl 0.0.0.0:443 --path404 /source/dist/404.html --cert /source/key/wmproxy.net.pem --key /source/key/wmproxy.net.key
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./key/:/source/key:rw
      - ./dist/:/source/dist:rw

networks:
  default:
    name: wmproxy-network

然后我们就可以访问我们的美美的官网啦。

image.png

image.png
完美适配小屏幕大屏幕,
官网网址
当然现在还是粗糙版本。

总结

一个项目总该配一个官网,从现在开始配一个可以自定义的官网,vuepress可以帮助我们搞一个好看的官网。

点击
[关注]

[在看]

[点赞]
是对作者最大的支持

本文作者:Wyu-Cnk

前言

最近在玩
图灵完备(Turing Complete)
一路过关斩将, 来到
机器赛跑(Robot Racing)
这一关的时候, 一看地图

image

对于选修过分形几何的我来说, 这不就是熟悉的
希尔伯特曲线
嘛! 老朋友了! 于是我复活已经死去的和分形几何有关的记忆, 用
分形
的思想初步实现了对应的汇编代码并通过了这一关。 正当我沾沾自喜并在网上查看其他人解决这一关的思路的时候, 我看到了这一篇文章:
图灵完备(Turing Complete)机器赛跑(Robot Racing)关卡纯汇编60字节达成成就纪念
, 我才知道原来有个成就是要求汇编代码在 64 个字节内过关, 并且这篇文章里用非常简洁巧妙的方法达成了这个成就。

image

我和我弟弟一起探讨研究了这个方法的原理, 现在我们都理解了这个方法并直呼”Vocal! “, 文章里的方法本质上也是用到了分形的思想, 但文章的作者只用了简单两句”容易发现, 有明显的递归模式“和”注意到 r1、r2 可被合并优化“, 难倒一众英雄好汉, 仿佛该作者一眼看透这个方法的本质, 并觉得这非常简单无需多言(这就是大佬么.jpg)。 而后我在这篇文章的启发下, 也获得了“小机快跑”的成就。
本文将用尽量通俗易懂的语言, 为了解过和没了解过图灵完备和分形的读者讲解用分形思想来通过机器赛跑这一关并达成成就“小机快跑”的思路, 同时也将给出实现该思路的汇编代码。

图灵完备

《图灵完备》是一款电路模拟器游戏, 于 2021 年在 Steam 上架。 在这款游戏中, 玩家可以从零开始构建计算机并编程它。 在解决谜题的过程中, 玩家将会学习到从基础逻辑门到算术单元、 存储器等复杂元件的知识, 并沿着这条道路最终学习如何搭建完整的处理器架构。 完成所有主线关卡后, 玩家将对处理器架构、 汇编语言和电子元件彼此之间的具体联系产生更加深刻的理解。 玩家也会了解高级编程语言中常见的条件判断、 循环、 函数等概念是如何在汇编和硬件层面具体实现的。
在本文中, 读者无需关注电路细节, 只需要关注算法中体现的分形的思想, 以及如何巧妙地简化算法即可。

机器赛跑是最后一章
汇编挑战
里的一关, 它要求玩家用汇编代码输出指令控制机器人移动走出迷宫, 而“小机快跑”成就则要求玩家编写的汇编代码要尽可能简短,
只有当代码字节数不超过64才可获得该成就

image

分形与希尔伯特曲线

分形通常被定义为“一个粗糙或零碎的几何形状, 可以分成数个部分, 且每一部分都(至少近似地)是整体缩小后的形状
[1]
”, 即具有自相似的性质, 例如最常见的雪花就是一种分形。 而在机器赛跑这一关里, 机器人所在的迷宫路线正好是分形几何里很经典的一种分形:
希尔伯特曲线
。 希尔伯特曲线一种能填充满一个平面正方形的分形曲线(空间填充曲线), 由大卫·希尔伯特在1891年提出。 称
\(H_n\)
为一条
希尔伯特曲线
, 如果
\(H_n\)
满足以下性质:

  1. \(H_0\)
    为正方形的中心点;

  2. \(H_n\)
    等比例缩小到原来的
    \(\frac{1}{4}\)
    , 记为
    \(H^{\prime}_n\)
    。 记
    \(H^{\prime}_n\)
    最靠近左下角的点为
    \(s_n\)
    , 最靠近右下角的点为
    \(t_n\)

    \(H_{n+1}\)
    按照以下方式连接生成:
    1. 将正方形等分成
      \(2\times2\)
      个小正方形;
    2. 令左下角的小正方形为顺时针旋转
      \(90\degree\)
      后的
      \(H^{\prime}_n\)
    3. 令左上方和右上方的小正方形为
      \(H^{\prime}_n\)
    4. 令右下方的小正方形为逆时针旋转
      \(90\degree\)
      后的
      \(H^{\prime}_n\)
    5. 连接左下方的
      \(s_n\)
      与左上方的
      \(s_n\)
    6. 连接左上方的
      \(t_n\)
      与右上方的
      \(s_n\)
    7. 连接右上方的
      \(t_n\)
      与右下方的
      \(t_n\)

image

image

可以严格证明,

\(n\rightarrow\infty\)
时,
\(H_n\)
会在正方形中稠密,即会填满正方形平面。

如果将正方形等分成
\(2^n\times2^n\)
个小正方形, 则容易看出,
\(H_n\)
具有以下性质:

  1. \(H_n\)
    会经过所有小正方形的中心点, 并且每个小正方形只会经过一次;
  2. 相邻的两个小正方形之间至多有一条连线;
  3. 不相邻的两个小正方形之间没有连线;
  4. \(H_n\)
    为一条折线, 两个端点分别位于最左下角和最右下角的小正方形中心点;
  5. \(H_n\)
    是没有方向的;
  6. \(H_n\)
    是左右对称的。

正文

有向希尔伯特曲线

虽然说迷宫的路线是希尔伯特曲线, 但实际上是有一点区别的,
\(H_n\)
是没有方向的, 但迷宫路径是有方向的, 是要从左下角的起点走到右下角的终点的。 因此我们需要仿照
\(H_n\)
的定义, 给出有方向的希尔伯特曲线的定义。


\(\hat{H}_n\)
为一条
有向希尔伯特曲线
, 如果
\(\hat{H}_n\)
满足以下性质:

  1. \(\hat{H}_0\)
    为正方形的中心点;

  2. \(\hat{H}_n\)
    等比例缩小到原来的
    \(\frac{1}{4}\)
    , 记为
    \(\hat{H}^{\prime}_n\)
    。记
    \(\hat{H}^{\prime}_n\)
    的起点为
    \(\hat{s}_n\)
    , 终点为
    \(\hat{t}_n\)

    \(\hat{H}_{n+1}\)
    按照以下方式连接生成:
    1. 将正方形等分成
      \(2\times2\)
      个小正方形;
    2. 令左下角的小正方形为路径反转并顺时针旋转
      \(90\degree\)
      后的
      \(\hat{H}^{\prime}_n\)
    3. 令左上方和右上方的小正方形为
      \(\hat{H}^{\prime}_n\)
    4. 令右下方的小正方形为路径反转并逆时针旋转
      \(90\degree\)
      后的
      \(\hat{H}^{\prime}_n\)
    5. 连接左下方的
      \(\hat{s}_n\)
      与左上方的
      \(\hat{s}_n\)
      , 方向指向左上方的
      \(\hat{s}_n\)
    6. 连接左上方的
      \(\hat{t}_n\)
      与右上方的
      \(\hat{s}_n\)
      , 方向指向右上方的
      \(\hat{s}_n\)
    7. 连接右上方的
      \(\hat{t}_n\)
      与右下方的
      \(\hat{t}_n\)
      , 方向指向右下方的
      \(\hat{t}_n\)

这里的路径反转指的是将路径从原本的起点走到终点改为从终点走到起点。 而要进行路径反转的原因是, 在拼接路径的时候, 需要前一条路径的终点走到后一条路径的起点, 这样构成的才是一条新的路径, 而如果不进行路径反转的话, 会出现“起点走到起点”或“终点走到终点”的情况, 这样得到的就不是一个新的路径了。

如果将正方形等分成
\(2^n\times2^n\)
个小正方形, 则容易看出,
\(\hat{H}_n\)

\(H_n\)
有许多相似的性质:

  1. \(\hat{H}_n\)
    从起点出发, 会经过所有小正方形的中心点, 并且每个小正方形只会经过一次, 而后走到终点;
  2. 相邻的两个小正方形之间至多有一条路径;
  3. 不相邻的两个小正方形之间没有路径;
  4. \(\hat{H}_n\)
    的起点和终点分别位于最左下角和最右下角的小正方形中心点;
  5. \(\hat{H}_n\)
    是有方向的, 从起点指向终点;
  6. \(\hat{H}_n\)
    进行路径反转等价于
    \(\hat{H}_n\)
    进行左右镜像翻转

其中性质6尤为重要, 它是由
\(H_n\)
的左右对称性决定的:
如果一条路径在忽略方向的情况下是左右对称的, 则这条路径进行路径反转等价于这条路径进行左右镜像翻转
。 而这个性质也意味着可以用更简便的方法来处理这个操作。

生成迷宫路径

容易看出, 迷宫路径实际上就是
\(\hat{H}_3\)
, 如果用给定的指令来表示路径的话,
\(\hat{H}_3\)
是这样表示的:

image

为了更形象地表示路径, 不妨记路径为指令 1
\(\rightarrow\)
指令 2
\(\rightarrow\dots\rightarrow\)
指令
\(m\)
, 例如
\(1\rightarrow2\rightarrow3\)
。 设
\(h(n)\)

\(\hat{H}_n\)
的路径表示, 则

\(\hat{H}_n\)
的生成方式 2~7

, 我们可以得到
\(h(n)\)
的生成方式(这里需要注意路径的先后顺序, 以方便后面按顺序输出路径):

  1. \(h(1)=3\rightarrow0\rightarrow1\)
  2. \(h(n+1)=f(h(n))\rightarrow3\rightarrow h(n)\rightarrow0\rightarrow h(n)\rightarrow1\rightarrow g(h(n))\)

其中
\(f\)
为将路径左右翻转后顺时针旋转
\(90\degree\)
的操作,
\(g\)
为将路径左右翻转后逆时针旋转
\(90\degree\)
的操作。 而要给出对路径的整体操作的定义, 实际上只需要给出对路径每一步的具体操作的定义即可。 因此这里只给出
\(f\)

\(g\)
针对每一步的具体定义:

\[f(x)=\begin{cases}
3, & x=0 \\
2, & x=1 \\
1, & x=2 \\
0, & x=3
\end{cases} \quad
,g(x)=\begin{cases}
1, & x=0 \\
0,& x=1 \\
3, & x=2 \\
2, & x=3
\end{cases} \quad.
\]

举例说明,
\(f(h(1))=f(3\rightarrow0\rightarrow1)=0\rightarrow3\rightarrow2\)

\(g(h(1))=g(3\rightarrow0\rightarrow1)=2\rightarrow1\rightarrow0\)
。 于是对于任意给定的正整数
\(n\)
, 我们都可以迭代生成对应的
\(h(n)\)
, 即
不管希尔伯特迷宫有多少级, 我们都可以用一条公式给它秒了!

汇编代码实现及优化

在用代码实现之前, 需要先简单了解一下什么是汇编语言。
汇编语言是一种低级编程语言, 用于与计算机硬件直接交互。 它是计算机指令集架构的一种表现形式, 使用符号代表计算机的机器指令。 在汇编语言中, 用助记符代替机器指令的操作码, 用地址符号或标号代替指令或操作数的地址。 汇编语言与计算机硬件的关系密切, 每一条汇编语句都对应着底层的机器指令, 直接操作计算机的寄存器和内存。

而《图灵完备》很形象地展示了汇编语言是如何操作寄存器和内存的, 不过这不是本节的重点, 本节只会摘取相关原理进行讲解。

位和字节

在游戏里, 一个 0-1 开关只有两种状态, 开或关, 因此可以用二进制来表示一个 0-1 开关。
一位
表示长度为 1 的二进制数, 而
一个字节
表示长度为 8 的二进制数, 即
1 字节=8 位

无符号 1 字节整数
的范围为 0~255, 本节提到的数据只在
无符号 1 字节
的范围内考虑。

寄存器

在游戏里, 寄存器可以简单理解成这样一个元件: 它可以读取或写入一个字节的数据, 读取和写入可以同时进行。

指令

在游戏里, 一个指令有 4 个字节, 分别是: **操作码, 参数 1, 参数 2, 结果地址。 **操作码和操作之间是一一对应的, 玩家可以在游戏里给不同的操作码起别名,以提高汇编代码的可读性; 参数 1 和参数 2 默认为寄存器地址, 程序会读取参数对应的寄存器内的值进行操作, 不过可以通过操作码指定参数为立即数, 此时会将参数视为参数本身进行操作; 结果地址则是指定操作的结果的存放地址(比如寄存器地址), 而当要进行的操作是条件跳转的时候, 结果地址指的是条件判断为真时要跳转到的地址。 以下为本节会用到的指令集

add
语法
add 参数 1  参数 2 结果地址
含义

结果地址对应的寄存器内的值 = 参数 1 对应的寄存器内的值 + 参数 2 对应的寄存器内的值

例子

add re0 re1 re2
表示
re2=re0+re1

imme1
语法
操作码|imme1 参数 1  参数 2 结果地址
含义

执行操作码的时候将参数 1 视为立即数

例子

add|imme1 1 re0 re1
表示
re1=1+re0

imme2
语法
操作码|imme2 参数 1  参数 2 结果地址
含义

执行操作码的时候将参数 2 视为立即数

例子

add|imme2 re0 1 re1
表示
re1=re0+1

sub
语法
sub 参数 1  参数 2 结果地址
含义

结果地址对应的寄存器内的值 = 参数 1 对应的寄存器内的值 - 参数 2 对应的寄存器内的值

例子

sub re0 re1 re2
表示
re2=re0-re1

and
语法
and 参数 1  参数 2 结果地址
含义

结果地址对应的寄存器内的值 = 参数1对应的寄存器内的值 &(按位与)参数 2 对应的寄存器内的值

例子

and re0 re1 re2
表示
re2=re0&re1

xor
语法
xor  参数 1  参数 2 结果地址
含义

结果地址对应的寄存器内的值 = 参数 1 对应的寄存器内的值 ^(按位异或)参数 2 对应的寄存器内的值

例子

xor re0 re1 re2
表示
re2=re0^re1

not
语法
xor  参数 1  参数 2 结果地址
含义

结果地址对应的寄存器内的值 = ~(按位非)参数 1 对应的寄存器内的值

例子

not re0 0 re1
表示
re1=~re0

条件跳转
语法
条件跳转操作码 参数 1  参数 2 跳转地址
含义

如果参数 1 和参数 2 按照条件跳转操作码对应的条件判断为真, 则跳转到跳转地址对应的指令

例子
label test
// 其他代码
equl re0 re1 test

表示如果
re0==re1
则跳转到
test
对应的指令处, 这里
label
会将当前指令的地址赋值给
test

equl
语法
equl 参数1  参数2 跳转地址
含义

如果
参数1 == 参数2
则跳转到跳转地址对应的指令

例子
equl re0 re1 test

表示如果
re0==re1
则跳转到 test 对应的指令处

call
语法
call 函数地址 _ _
含义

调用函数, 跳转到函数地址对应的指令

例子
label fun
# 其他代码
call fun 0 0

表示调用 fun 函数, 跳转到 fun 对应的指令处

return
语法
return _ _ _
含义

跳转到上一次执行的 call 语句的下一句指令

例子
call fun 0 0
add re0 re1 re2
// 其他代码
label fun
// 其他代码
return 0 0 0

在执行
call
语句后会跳转到
fun
对应的指令处, 当执行到
return
指令的时候, 会跳转到上一次执行
call
语句的下一句指令, 也就是
add
这一句

代码实现

本节的目标是以尽可能短的代码、 尽可能少的寄存器数量和尽可能短的运行时间来输出
\(h(n)\)
, 为此首先要对
\(f\)

\(g\)
进行处理。 容易验证
\(f(x)=(\sim x)\&3\)

\(g(x)=x\textasciicircum1\)
, 这里要用到按位与“&”、 按位取反“~”、 按位异或“^“运算, 均为简单的逻辑门运算, 代码简单, 运行速度快, 而且”
\(\&3\)
“本质上就是对 4 取余, 可以在不破坏值与指令一一对应关系的前提下将值限制在值的范围内。

下面给出的 Python 代码参考了:
图灵完备(Turing Complete)机器赛跑(Robot Racing)关卡纯汇编60字节达成成就纪念
:

n = 3 
r0 = 0 # 第几层递归, h(r0) 为第 n-r0 层递归
r1 = 0 # 当前路径方向

def fill():
  global r0, r1
  if r0 is not n:
    r0 += 1
    r1 = ~r1 # f
    fill() # f(h(n))
    print(r1 & 3, end='') # 对于当前层 n 的指令 3
    r1 = ~r1 # 回溯
    fill() # h(n)
    print(r1 & 3, end='') # 对于当前层 n 的指令 0
    fill() # h(n)
    r1 ^= 1 #g
    print(r1 & 3, end='') # 对于当前层 n 的指令 1
    fill() # g(h(n))
    r1 ^= 1 # 回溯
    r0 -= 1 # 回溯

fill()

fill() 实现的功能是输出以 r1 为起点朝向的 h(n-r0), 并且从终点出来后机器人的朝向仍为 r1。 注意到
\(f(f(x))=x,g(g(x))=x\)
, 因此递归的回溯只需要再调用一次
\(f\)

\(g\)
即可。

将其翻译为汇编语言如下:

label fill
equl|imme2 re0 3 end
add|imme2 re0 1 re0
not|imme2 re1 0 re1 
call fill 0 0
and|imme2 re1 3 out
not|imme2 re1 0 re1
call fill 0 0
and|imme2 re1 3 out 
call fill 0 0
xor|imme2 re1 1 re1 
and|imme2 re1 3 out
call fill 0 0
xor|imme2 re1 1 re1
sub|imme2 re0 1 re0
label end
return 0 0 0

只需要 60 个字节, 达成成就”小机快跑“! 而且只用到两个寄存器, 并且运行时间也很短, 非常优雅~

结语

汇编作为较底层的编程语言, 其直接操作内存的方式赋予了这门语言独特的魅力。 在优化
\(f\)

\(g\)
的过程中, 我发现缺乏一定的 ”汇编直觉“ 或 ”逻辑门直觉'“是很难一眼看出优化方案的。 一开始, 我采用了较为笨拙的方法来实现这两个映射, 直到阅读了那篇”容易看出“的文章后, 我才豁然开朗, 原来还能以这样的方式操作!

与此同时, 分形是一门令人着迷的学科, 其美丽和奇妙之处让人为之倾倒。 通过这篇文章, 我希望能够激发更多人发现这枚数学瑰宝的美丽。 同时, 我也鼓励图灵完备的玩家们努力实现 ”小机快跑“ 的成就!

参考文献


  1. Mandelbrot Benoit B. 1983. The Fractal Geometry of Nature. [New edition] ed. New York: Freeman.
    ↩︎

在本文中,我们将使用Python编写一个简单的日历程序。虽然市面上已经存在现成的日历功能,并且有第三方库可以直接调用实现,但我们仍然希望通过自己编写日历程序来引出我认为好用的日历实现。希望这篇文章能够对你有所帮助。

在Python官方文档中,我们可以找到一个名为"calendar"的模块,它可以轻松实现一个简易的日历,满足基本需求。此外,我们还经常听说过一个名为"Tkinter"的简易图形用户界面(GUI)库,虽然它是官方提供的最简单的库之一,但很多人表示它存在一些坑,因此我们需要谨慎使用并自行甄别。为了避免这些坑,我会使用大家推荐的PyQt库来实现一个简易版的日历。最后,我还会分析和借鉴大家开源的日历实现,以便给大家更多的思路和参考。希望这篇文章能够对你有所帮助,并且能够引导你选择合适的日历实现方式。

所以,今天我们的主题仍然是关于使用Python实现日历的内容。接下来,让我们一起来探索一下吧!

calendar

对于实现基本的日历需求,官方的calendar模块已经提供了很好的解决方案。下面是一个示例代码,你可以自行运行一下来查看结果。

# 引入日历模块
import calendar
 
# 输入指定年月
yy = int(input("输入年份: "))
mm = int(input("输入月份: "))
 
# 显示日历
print(calendar.month(yy,mm))

Tkinter

再来介绍一下,Tkinter库是一款广泛使用的图形用户界面库。作为Python标准库的一部分,Tkinter无需额外安装,非常方便。此外,由于Tkinter拥有大量的文档和教程资源,使得学习和使用Tkinter变得更加容易。然而,需要注意的是,Tkinter的默认外观相对简单和基础,如果想要创建更具吸引力和专业的界面,可能需要额外的努力和技巧。

这个库适用于开发小到中等规模的应用程序,特别适合初学者和快速原型开发。然而,对于复杂的应用程序或对界面外观有更高要求的项目,可能需要考虑其他更专业的GUI库。实现起来可能会有些困难,但我会尽量简单地创建一个库,不过不要对自己要求太高,除非真的有这方面的需求。

import tkinter as tk
import calendar
import datetime

class CalendarApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Calendar")
        self.root.geometry("400x400")
        
        self.current_date = datetime.date.today()
        self.year = self.current_date.year
        self.month = self.current_date.month
        
        self.calendar_frame = tk.Frame(self.root)
        self.calendar_frame.pack(pady=20)
        
        self.prev_button = tk.Button(self.calendar_frame, text="<<", command=self.prev_month)
        self.prev_button.grid(row=0, column=0, padx=10)
        
        self.month_year_label = tk.Label(self.calendar_frame, text=f"{calendar.month_name[self.month]} {self.year}",
                                         font=("Helvetica", 16, "bold"))
        self.month_year_label.grid(row=0, column=1)
        
        self.next_button = tk.Button(self.calendar_frame, text=">>", command=self.next_month)
        self.next_button.grid(row=0, column=2, padx=10)
        
        self.calendar = tk.Text(self.root, width=20, height=10, font=("Helvetica", 14))
        self.calendar.pack(pady=20)
        
        self.update_calendar()
        
    def prev_month(self):
        self.month -= 1
        if self.month == 0:
            self.year -= 1
            self.month = 12
        self.update_calendar()
        
    def next_month(self):
        self.month += 1
        if self.month == 13:
            self.year += 1
            self.month = 1
        self.update_calendar()
        
    def update_calendar(self):
        self.month_year_label.config(text=f"{calendar.month_name[self.month]} {self.year}")
        self.calendar.delete(1.0, tk.END)
        
        cal = calendar.monthcalendar(self.year, self.month)
        for week in cal:
            week_str = ""
            for day in week:
                if day == 0:
                    week_str += "   "
                else:
                    week_str += f"{day:2d} "
            self.calendar.insert(tk.END, week_str + "\n")
            
if __name__ == "__main__":
    root = tk.Tk()
    app = CalendarApp(root)
    root.mainloop()

这段示例代码是一个简单的日历应用程序,使用了Tkinter库来创建用户界面。它展示了当前月份的日历,并提供了向前和向后浏览月份的按钮。你可以根据自己的需求来进行修改和扩展,以满足更多的功能和用户体验。

PyQt

在我个人的观点中,我认为PyQt虽然并不是Python标准库的一部分。需要单独安装,但这并不算是一个缺点。相较于Tkinter,学习和使用PyQt可能会更具挑战性,因为它的学习曲线相对陡峭。然而,一旦熟悉了PyQt的编写方式,编写代码会变得非常舒适。它相对于其他框架来说更加人性化。

当你仔细阅读完这段日历代码后,你会感到非常惊艳,而且页面的优化效果也是非常显著的。

import sys
from PyQt6.QtWidgets import QApplication, QWidget, QCalendarWidget, QVBoxLayout

class CalendarApp(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("小雨版Calendar App")
        self.setGeometry(100, 100, 300, 300)
        self.initUI()

    def initUI(self):
        layout = QVBoxLayout(self)
        calendar = QCalendarWidget(self)
        layout.addWidget(calendar)
        self.setLayout(layout)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = CalendarApp()
    window.show()
    sys.exit(app.exec())

基本功能已经完全实现了。在第一列数字中显示的是今年的第几周。除此之外,用户还可以选择月份并直接修改年份。总的来说,该功能具有明显的优点。

image

borax

他也是一个第三方库,但是它是专门用来制作日历的,并且以开源的形式提供。如果你有相关的需求,可以参考它。此外,这个库还非常简单易用,只需要安装即可开始使用。

从 Borax 4.1.0 开始,Borax 提供两个基于 Borax.Calendar 的日历应用。

应用程序 功能 启动命令
日历应用 公农历日期显示,及其他日期工具 python -m borax.capp
节日创建器 创建节日库 python -m borax.capp creator

为什么要创建一个节日库呢?就算没有节日,你也可以创建一个生日库,它还自带倒计时功能,这样你就不会老是忘记某些人的生日了。非常方便实用,我来教大家如何使用。

首先直接安装
pip install borax

安装好borax之后,你可以直接在控制台中使用命令
python -m borax.capp
。这样就能成功显示倒计时、节日以及农历等功能。这些功能都已经得到了很好的优化和完善。

image

创建生日我就不演示了。按照你的要求创建一个即可。

其他

我也在GitHub上找到了一些相对冷门的项目,希望它们能对你有所帮助。

比如:OpenHappyHackingCalendar-Python

他是根据别人的库通过Python语言进行了一些改编,我也认为它还不错,所以拿出来让你看看。不过需要使用爬虫功能,并且需要有网络连接的条件,所以请注意。当你按照要求安装好环境并配置好年月份后,直接运行index.py文件,就可以生成对应的html文件了。

image

总结

通过本文,我们使用Python编写了一个简单的日历程序。在Python官方文档中,我们找到了一个名为"calendar"的模块,它可以轻松实现一个简易的日历,满足基本需求。此外,我们还介绍了Tkinter和PyQt两个常用的图形用户界面库,以及borax库和其他一些开源项目来实现更多功能和用户体验的日历应用。希望本文对你有所帮助,能够引导你选择合适的日历实现方式。