跳转至

MyBlog

使用七牛云api下载图片

先安装七牛云sdk

Bash
pip install qiniu
Bash
1
2
3
4
requests>=2.28.0
beautifulsoup4>=4.11.0
lxml>=4.9.0
pathlib2>=2.3.7; python_version < '3.4'

修改config数据为个人数据

Success
Python
   #!/usr/bin/env python3
   # -*- coding: utf-8 -*-

   """
   使用七牛云API获取文件列表并下载图片文件

   这是推荐的方式,比解析HTML页面更可靠
   需要七牛云的AccessKey和SecretKey
   """

   import os
   import sys
   import time
   import requests
   from pathlib import Path
   from urllib.parse import urljoin

   try:
       from qiniu import Auth, BucketManager
       QINIU_SDK_AVAILABLE = True
   except ImportError:
       QINIU_SDK_AVAILABLE = False
       print("警告:未安装qiniu SDK,请运行: pip install qiniu")

   class QiniuAPIDownloader:
       def __init__(self, access_key, secret_key, bucket_name, download_dir="downloaded_images"):
           """
           初始化七牛云API下载器

           Args:
               access_key: 七牛云AccessKey
               secret_key: 七牛云SecretKey  
               bucket_name: bucket名称
               download_dir: 本地下载目录
           """
           self.access_key = access_key
           self.secret_key = secret_key
           self.bucket_name = bucket_name
           self.download_dir = Path(download_dir)

           # 支持的图片格式
           self.image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.ico', '.tiff', '.tif'}

           # 创建下载目录
           self.download_dir.mkdir(parents=True, exist_ok=True)

           # 初始化七牛云认证
           if QINIU_SDK_AVAILABLE:
               self.auth = Auth(access_key, secret_key)
               self.bucket_manager = BucketManager(self.auth)
           else:
               self.auth = None
               self.bucket_manager = None

       def get_file_list(self, prefix="", limit=1000):
           """
           获取bucket中的文件列表

           Args:
               prefix: 文件前缀过滤
               limit: 获取文件数量限制

           Returns:
               list: 文件信息列表
           """
           if not QINIU_SDK_AVAILABLE:
               print("错误:需要安装qiniu SDK")
               return []

           try:
               print(f"正在获取bucket '{self.bucket_name}' 中的文件列表...")
               if prefix:
                   print(f"前缀过滤: {prefix}")

               all_files = []
               marker = None

               while True:
                   ret, eof, info = self.bucket_manager.list(
                       self.bucket_name, 
                       prefix=prefix, 
                       marker=marker,
                       limit=limit
                   )

                   if ret is None:
                       print(f"获取文件列表失败: {info}")
                       break

                   files = ret.get('items', [])
                   all_files.extend(files)

                   if eof:
                       break

                   marker = ret.get('marker')

               print(f"找到 {len(all_files)} 个文件")
               return all_files

           except Exception as e:
               print(f"获取文件列表失败: {e}")
               return []

       def filter_image_files(self, file_list):
           """
           筛选出图片文件

           Args:
               file_list: 文件信息列表

           Returns:
               list: 图片文件信息列表
           """
           image_files = []

           for file_info in file_list:
               filename = file_info.get('key', '')
               file_ext = Path(filename).suffix.lower()

               if file_ext in self.image_extensions:
                   image_files.append(file_info)

           print(f"筛选出 {len(image_files)} 个图片文件")
           return image_files

       def get_download_url(self, filename, domain=None, expires=3600):
           """
           获取文件下载URL

           Args:
               filename: 文件名
               domain: 绑定的域名,如果为None则使用默认域名
               expires: URL过期时间(秒)

           Returns:
               str: 下载URL
           """
           if not QINIU_SDK_AVAILABLE:
               return None

           try:
               if domain:
                   # 使用自定义域名
                   if not domain.startswith('http'):
                       domain = f"http://{domain}"
                   base_url = domain.rstrip('/')
                   download_url = self.auth.private_download_url(f"{base_url}/{filename}", expires=expires)
               else:
                   # 使用默认域名(需要配置)
                   # 这里需要替换为实际的域名
                   print("警告:未指定下载域名,请提供bucket绑定的域名")
                   return None

               return download_url

           except Exception as e:
               print(f"生成下载URL失败 {filename}: {e}")
               return None

       def download_file(self, file_info, domain=None):
           """
           下载单个文件

           Args:
               file_info: 文件信息字典
               domain: 下载域名

           Returns:
               bool: 下载是否成功
           """
           filename = file_info.get('key', '')
           filesize = file_info.get('fsize', 0)

           try:
               # 获取下载URL
               download_url = self.get_download_url(filename, domain)
               if not download_url:
                   print(f"无法获取下载URL: {filename}")
                   return False

               print(f"正在下载: {filename} ({filesize} bytes)")

               # 下载文件
               response = requests.get(download_url, stream=True, timeout=60)
               response.raise_for_status()

               # 创建本地文件路径
               local_path = self.download_dir / filename
               local_path.parent.mkdir(parents=True, exist_ok=True)

               # 保存文件
               with open(local_path, 'wb') as f:
                   for chunk in response.iter_content(chunk_size=8192):
                       f.write(chunk)

               print(f"下载完成: {local_path}")
               return True

           except Exception as e:
               print(f"下载失败 {filename}: {e}")
               return False

       def download_images(self, prefix="images/", domain=None):
           """
           下载所有图片文件

           Args:
               prefix: 文件前缀过滤
               domain: 下载域名
           """
           if not QINIU_SDK_AVAILABLE:
               print("错误:请安装qiniu SDK: pip install qiniu")
               return

           print("=== 七牛云API图片下载器 ===\n")

           # 1. 获取文件列表
           file_list = self.get_file_list(prefix)
           if not file_list:
               print("未找到任何文件")
               return

           # 2. 筛选图片文件
           image_files = self.filter_image_files(file_list)
           if not image_files:
               print("未找到图片文件")
               return

           print(f"\n找到的图片文件:")
           for i, file_info in enumerate(image_files, 1):
               filename = file_info.get('key', '')
               filesize = file_info.get('fsize', 0)
               print(f"{i:3d}. {filename} ({filesize} bytes)")

           if not domain:
               print("\n错误:请提供bucket绑定的域名")
               print("例如: your-bucket.domain.com")
               return

           # 3. 下载文件
           print(f"\n开始下载到: {self.download_dir.absolute()}")

           downloaded_count = 0
           failed_count = 0

           for file_info in image_files:
               if self.download_file(file_info, domain):
                   downloaded_count += 1
               else:
                   failed_count += 1

               # 添加延迟避免请求过快
               time.sleep(0.2)

           print(f"\n下载总结:")
           print(f"成功下载: {downloaded_count} 个文件")
           print(f"下载失败: {failed_count} 个文件")
           print(f"文件保存在: {self.download_dir.absolute()}")

   def main():
       """主函数"""
       print("七牛云API下载器")
       print("=" * 30)

       # 配置信息 - 请填入您的七牛云密钥信息
       config = {
           'access_key': '',  # 请填入您的AccessKey
           'secret_key': '',  # 请填入您的SecretKey  
           'bucket_name': '',  # bucket名称
           'prefix': 'images/',  # 文件前缀
           'domain': '',  # 请填入bucket绑定的域名,如: example.com
       }

       # 检查配置
       if not config['access_key'] or not config['secret_key']:
           print("请配置七牛云密钥信息:")
           print("1. 登录七牛云控制台")
           print("2. 在个人中心 -> 密钥管理中获取AccessKey和SecretKey")
           print("3. 将密钥信息填入此脚本的config字典中")
           return

       if not config['domain']:
           print("请配置bucket的访问域名:")
           print("1. 在七牛云控制台的bucket设置中找到绑定的域名")
           print("2. 将域名填入此脚本的config字典中")
           print("   格式如: example.com 或 cdn.example.com")
           return

       # 创建下载器并执行下载
       downloader = QiniuAPIDownloader(
           access_key=config['access_key'],
           secret_key=config['secret_key'], 
           bucket_name=config['bucket_name']
       )

       downloader.download_images(
           prefix=config['prefix'],
           domain=config['domain']
       )

   if __name__ == "__main__":
       main()

def main():函数中config数据根据实际情况填写。

mkdocs快速搭建帮助文档(以气泡图为例)

插件安装

Bash
1
2
3
pip install mkdocs-material

mkdocs new .
Bash
1
2
3
4
.
├─ docs/
  └─ index.md
└─ mkdocs.yml
graph TD
    Root[项目根目录]
    Root --- Docs[docs/ 文件夹]
    Root --- MkdocsYml[mkdocs.yml 文件]
    Docs --- IndexMd[index.md 文件]

创建文件夹目录

例如E:\Gitee\helperdoc\BubbleDiagram

Bash
mkdocs new .

image-20250923103807473

Besides, further assets may also be put in the overrides directory:

overrides
Bash
    .
    ├─ .icons/                      # Bundled icon sets
    ├─ assets/
      ├─ images/                   # Images and icons
      ├─ javascripts/              # JavaScript files
      └─ stylesheets/              # Style sheets
    ├─ partials/
      ├─ integrations/             # Third-party integrations
        ├─ analytics/             # Analytics integrations
        └─ analytics.html         # Analytics setup
      ├─ languages/                # Translation languages
      ├─ actions.html              # Actions
      ├─ alternate.html            # Site language selector
      ├─ comments.html             # Comment system (empty by default)
      ├─ consent.html              # Consent
      ├─ content.html              # Page content
      ├─ copyright.html            # Copyright and theme information
      ├─ feedback.html             # Was this page helpful?
      ├─ footer.html               # Footer bar
      ├─ header.html               # Header bar
      ├─ icons.html                # Custom icons
      ├─ language.html             # Translation setup
      ├─ logo.html                 # Logo in header and sidebar
      ├─ nav.html                  # Main navigation
      ├─ nav-item.html             # Main navigation item
      ├─ pagination.html           # Pagination (used for blog)
      ├─ palette.html              # Color palette toggle
      ├─ post.html                 # Blog post excerpt
      ├─ progress.html             # Progress indicator
      ├─ search.html               # Search interface
      ├─ social.html               # Social links
      ├─ source.html               # Repository information
      ├─ source-file.html          # Source file information
      ├─ tabs.html                 # Tabs navigation
      ├─ tabs-item.html            # Tabs navigation item
      ├─ tags.html                 # Tags
      ├─ toc.html                  # Table of contents
      ├─ toc-item.html             # Table of contents item
      └─ top.html                  # Back-to-top button
    ├─ 404.html                     # 404 error page
    ├─ base.html                    # Base template
    ├─ blog.html                    # Blog index page
    ├─ blog-archive.html            # Blog archive index page
    ├─ blog-category.html           # Blog category index page
    ├─ blog-post.html               # Blog post page
    └─ main.html                    # Default page

修改mkdocs.yml文件

Success
YAML
  site_name: 我的帮助文档
  site_description: 关于mkdocs-material支持的markdown语法,包括传统语法和扩展语法
  site_author: JerryMa
  site_url: http://127.0.0.1:8000
  theme:
    name: material
    palette:
      # Toggle light mode
      - scheme: default
        primary: Blue Grey
        accent: Pink
        toggle:
          icon: material/toggle-switch
          name: 切换到明亮模式
      # Toggle dark mode
      - scheme: slate
        primary: blue
        accent: amber
        toggle:
          icon: material/toggle-switch-off-outline
          name: 切换到暗黑模式
    features:
      - announce.dismiss
      - content.tabs.link
      - content.tooltips
      - content.code.copy #代码复制
      - content.code.select
      - content.code.annotate   
      - content.footnote.tooltips
      - header.autohide
      - navigation.footer
      - navigation.indexes
      - navigation.instant
      - navigation.instant.prefetch
      - navigation.instant.progress
      - navigation.prune
      - navigation.sections
      - navigation.tabs
      - navigation.tabs.sticky
      - navigation.top # 返回顶部的按钮 在上滑时出现  
      - navigation.tracking
      - search.highlight # 搜索出的文章关键词加入高亮
      - search.share #搜索分享按钮   
      - search.suggest # 搜索输入一些字母时推荐补全整个单词
      - toc.follow
      - toc.integrate
    language: 'zh'
  plugins:
    - offline
    - search:
        lang: 
          - zh
          - en
        separator: '[\s\-\.]+'
    - minify:
        minify_html: true
        minify_js: true
        minify_css: true
        htmlmin_opts:
          remove_comments: true
        css_files:
          - stylesheets/extra.css
    - glightbox:
        touchNavigation: true
        loop: false
        effect: zoom
        slide_effect: slide
        width: 100%
        height: auto
        zoomable: true
        draggable: true
        skip_classes:
          - custom-skip-class-name
        auto_caption: false
        caption_position: bottom
  extra:
    social:
      - icon: fontawesome/brands/github #联系方式图标 : https://fontawesome.com/ 去这里找图标
        link: https://github.com/mazaiguo
        name: JerryMa on Github
      - icon: fontawesome/brands/gitlab
        link: https://gitlab.zwsoft.cn/mazaiguo
      - icon: fontawesome/regular/envelope
        link: mailto:mazaiguo@126.com
        name: Email
    analytics:
      feedback:
        title: 这个页面对您有帮助吗?
        ratings:
          - icon: material/emoticon-happy-outline
            name: 有帮助
            data: 1
            note: >-
              感谢您的反馈!
          - icon: material/emoticon-sad-outline
            name: 可以改进
            data: 0
            note: >-
              感谢您的反馈!请帮助我们改进这个页面,
              <a href="https://github.com/mazaiguo/mazaiguo.github.io/issues/new/?title=[Feedback]+{title}+-+{url}" target="_blank" rel="noopener">告诉我们需要改进的地方</a>。
    generator: false #是否删除页脚显示"使用 MkDocs 材料制造"
  extra_javascript:
    - javascripts/katex.js
    - https://unpkg.com/katex@0/dist/katex.min.js
    - https://unpkg.com/katex@0/dist/contrib/auto-render.min.js
    #- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js
    #- javascripts/config.js
  extra_css:
    #- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/default.min.css
    - stylesheets/extra.css  
    - https://unpkg.com/katex@0/dist/katex.min.css
  markdown_extensions:
    - abbr
    - admonition
    - attr_list
    - def_list
    - footnotes
    - md_in_html
    - meta
    - tables
    - toc:
        permalink: true
        title: 目录
    - pymdownx.arithmatex:
        generic: true
    - pymdownx.betterem:
        smart_enable: all
    - pymdownx.caret
    - pymdownx.details
    - pymdownx.emoji:
        emoji_generator: !!python/name:material.extensions.emoji.to_svg
        emoji_index: !!python/name:material.extensions.emoji.twemoji
    - pymdownx.highlight:
        anchor_linenums: true
        line_spans: __span
        pygments_lang_class: true
        linenums: true
        linenums_style: pymdownx.inline
        auto_title: true # 显示编程语言名称
        use_pygments: true
    - pymdownx.inlinehilite
    - pymdownx.keys
    - pymdownx.magiclink:
        normalize_issue_symbols: true
        repo_url_shorthand: true
        user: mazaiguo
        repo: helpdoc
    - pymdownx.mark
    - pymdownx.smartsymbols
    - pymdownx.snippets:
        check_paths: true
    - pymdownx.superfences:
        custom_fences:
          - name: mermaid
            class: mermaid
            format: !!python/name:pymdownx.superfences.fence_code_format
    - pymdownx.tabbed:
        alternate_style: true
        combine_header_slug: true
        slugify: !!python/object/apply:pymdownx.slugs.slugify
          kwds:
            case: lower
    - pymdownx.tasklist:
        custom_checkbox: true
    - pymdownx.tilde
    - pymdownx.critic
  copyright: Copyright &copy; 2016 - present JerryMa

image-20250923154538869

这几个是MKDOCS内置使用的,我们建立文件夹时不要与这些名字冲突了

增加一些配置文件

katex-docsjavascriptskatexjs

Note
JavaScript
   document$.subscribe(({ body }) => { 
     renderMathInElement(body, {
       delimiters: [
         { left: "$$",  right: "$$",  display: true },
         { left: "$",   right: "$",   display: false },
         { left: "\\(", right: "\\)", display: false },
         { left: "\\[", right: "\\]", display: true }
       ],
     })
   })

docs/javascripts/tablesort.js

Note
JavaScript
1
2
3
4
5
6
document$.subscribe(function() {
  var tables = document.querySelectorAll("article table:not([class])")
  tables.forEach(function(table) {
    new Tablesort(table)
  })
})

[docs\stylesheets\extra.css]

Warning
CSS
    :root > * {
      --md-code-hl-string-color: #0ff1ce;
      --md-code-hl-number-color: #ae81ff;
      --md-code-hl-special-color: #a846b9;
      --md-code-hl-function-color: #66d9ef;
      --md-code-hl-constant-color: #f92672;
      --md-code-hl-keyword-color: #f92672;
      --md-code-hl-string-color: #e6db74;
      --md-code-hl-name-color: #feffff;
      --md-code-hl-operator-color: #f92672;
      --md-code-hl-punctuation-color: #ffffff;
      --md-code-hl-comment-color: #757575;
      --md-code-hl-generic-color: #af82fc;
      --md-code-hl-variable-color: #f92672;
      --md-code-fg-color: #ffffff;
      --md-code-bg-color: #282c34;
      --md-code-hl-color: #ffff7f;
      --md-default-fg-color--light: #75715f;
    }
    .md-typeset p > code {
      background-color: #ffffff;
      color: #eb245c;
    }
    .md-typeset li code {
      background-color: #ffffff;
      color: #eb245c;
    }
    /*代码块头部图标 start*/
    .highlight span.filename pre:before {
      content: "";
      display: block;
      background: url(../assets/images/codeHeader.png);
      height: 30px;
      background-size: 40px;
      background-repeat: no-repeat;
      background-color: #212121;
      background-position: 10px 10px;
    }
    /*代码块头部图标 end*/

    .highlighttable .code pre > code {
      color: #c0c3c1;
      font-family: "Inconsolata", consolas, "PingFang SC", "Microsoft YaHei",
        monospace;
      background-color: #212121;
      font-size: 15px;
      white-space: pre;
      line-height: 1.5;
      -moz-tab-size: 4;
      -o-tab-size: 4;
      tab-size: 4;
    }
    .highlight span.filename {
      color: white;
    }

    /* 基础容器样式:确保导航项布局正常 */
    .md-nav__list {
      list-style: none;
      padding: 0;
      margin: 0;
      width: 280px; /* 适配侧边导航宽度,可按需调整 */
    }
    .md-nav__item {
      margin: 4px 0;
    }
    .md-nav__link {
      display: block;
      padding: 8px 12px;
      border-radius: 6px;
      text-decoration: none;
      color: #333; /* 默认文本色 */
      transition: all 0.3s ease; /* 统一过渡动画,确保流畅性 */
    }

    /* 核心:.md-ellipsis 交互特效 */
    .md-ellipsis {
      position: relative;
      z-index: 1;
      transition: color 0.3s ease;
    }
    /* 鼠标悬浮(hover)效果:文本变色 + 底部渐变下划线 */
    .md-nav__link:hover .md-ellipsis {
      color: #165dff; /* 悬浮文本主色(可替换为品牌色) */
    }
    .md-nav__link:hover .md-ellipsis::after {
      content: "";
      position: absolute;
      left: 0;
      bottom: -2px;
      width: 100%;
      height: 2px;
      background: linear-gradient(90deg, #165dff, #4080ff); /* 渐变下划线 */
      border-radius: 1px;
      transform: scaleX(1);
      transform-origin: left center;
      transition: transform 0.3s ease;
    }
    /* 初始状态:下划线收缩至0,hover时展开 */
    .md-ellipsis::after {
      content: "";
      position: absolute;
      left: 0;
      bottom: -2px;
      width: 100%;
      height: 2px;
      background: linear-gradient(90deg, #165dff, #4080ff);
      border-radius: 1px;
      transform: scaleX(0);
      transform-origin: left center;
      transition: transform 0.3s ease;
    }

    /* 鼠标点击(active)效果:文本加深 + 背景压暗 */
    .md-nav__link:active .md-ellipsis {
      color: #0e42d2; /* 点击文本加深色 */
      font-weight: 500; /* 点击时文本轻微加粗 */
    }
    .md-nav__link:active {
      background-color: rgba(22, 93, 255, 0.1); /* 点击背景色(淡蓝压暗) */
      transform: translateY(1px); /* 轻微下沉,模拟物理按压感 */
      transition: transform 0.1s ease, background-color 0.1s ease;
    }

    /* 激活状态(.md-nav__link--active):区分当前选中项 */
    .md-nav__link--active .md-ellipsis {
      color: #165dff;
      font-weight: 500;
    }
    .md-nav__link--active .md-ellipsis::after {
      transform: scaleX(1); /* 激活项默认显示下划线 */
    }
    .md-nav__link--active {
      background-color: rgba(22, 93, 255, 0.05); /* 激活项背景色 */
    }

    /* 标签云样式 */
    .tag-cloud {
      margin: 1rem 0;
      line-height: 2;
    }

    .tag-cloud .tag {
      display: inline-block;
      padding: 0.25rem 0.5rem;
      margin: 0.125rem;
      background-color: var(--md-primary-fg-color--light);
      color: var(--md-primary-bg-color);
      border-radius: 0.25rem;
      text-decoration: none;
      font-size: 0.875rem;
      font-weight: 500;
      transition: all 0.2s ease;
    }

    .tag-cloud .tag:hover {
      background-color: var(--md-primary-fg-color);
      transform: translateY(-1px);
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }

    /* 暗色主题下的标签样式 */
    [data-md-color-scheme="slate"] .tag-cloud .tag {
      background-color: var(--md-accent-fg-color);
      color: var(--md-default-bg-color);
    }

    [data-md-color-scheme="slate"] .tag-cloud .tag:hover {
      background-color: var(--md-accent-fg-color--transparent);
    }

    /* 博客卡片样式 */
    .blog-card {
      background: var(--md-default-bg-color);
      border: 1px solid var(--md-default-fg-color--lightest);
      border-radius: 0.5rem;
      padding: 1.5rem;
      margin: 1rem 0;
      transition: all 0.2s ease;
    }

    .blog-card:hover {
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
      transform: translateY(-2px);
    }

    /* 统计数字样式 */
    .stats-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 1rem;
      margin: 2rem 0;
    }

    .stat-item {
      text-align: center;
      padding: 1rem;
      background: var(--md-default-bg-color);
      border: 1px solid var(--md-default-fg-color--lightest);
      border-radius: 0.5rem;
    }

    .stat-number {
      font-size: 2rem;
      font-weight: bold;
      color: var(--md-primary-fg-color);
      display: block;
    }

    .stat-label {
      font-size: 0.875rem;
      color: var(--md-default-fg-color--light);
      margin-top: 0.5rem;
    }

    /* 分类页面样式 */
    .category-section {
      margin: 2rem 0;
    }

    .category-title {
      color: var(--md-primary-fg-color);
      border-bottom: 2px solid var(--md-primary-fg-color--light);
      padding-bottom: 0.5rem;
      margin-bottom: 1rem;
    }

    .category-list {
      list-style: none;
      padding: 0;
    }

    .category-list li {
      margin: 0.5rem 0;
      padding-left: 1rem;
      border-left: 3px solid var(--md-accent-fg-color);
    }

    .category-list a {
      text-decoration: none;
      color: var(--md-default-fg-color);
      font-weight: 500;
    }

    .category-list a:hover {
      color: var(--md-primary-fg-color);
    }

    /* 响应式设计 */
    @media screen and (max-width: 768px) {
      .tag-cloud .tag {
        font-size: 0.75rem;
        padding: 0.2rem 0.4rem;
      }

      .stats-grid {
        grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
      }

      .stat-number {
        font-size: 1.5rem;
      }
    }

    /* 代码块优化 */
    .highlight pre {
      border-radius: 0.5rem;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }

    /* 文章元信息样式 */
    .article-meta {
      display: flex;
      flex-wrap: wrap;
      gap: 1rem;
      margin: 1rem 0;
      padding: 1rem;
      background: var(--md-code-bg-color);
      border-radius: 0.5rem;
      font-size: 0.875rem;
      color: var(--md-default-fg-color--light);
    }

    .article-meta .meta-item {
      display: flex;
      align-items: center;
      gap: 0.25rem;
    }

    .article-meta .meta-icon {
      width: 1rem;
      height: 1rem;
      opacity: 0.7;
    }

    /* 博客文章列表样式优化 */
    .md-content .md-typeset .md-post {
      margin-bottom: 2rem;
      padding-bottom: 2rem;
      border-bottom: 1px solid var(--md-default-fg-color--lightest);
    }

    .md-content .md-typeset .md-post:last-child {
      border-bottom: none;
    }

    /* 提高阅读体验 */
    .md-typeset h1,
    .md-typeset h2,
    .md-typeset h3 {
      margin-top: 2rem;
      margin-bottom: 1rem;
    }

    .md-typeset h1:first-child,
    .md-typeset h2:first-child,
    .md-typeset h3:first-child {
      margin-top: 0;
    }

[docs\assets\images\codeHeader.png]

codeHeader

帮助文档生成样式

image-20250923160659893

image-20250923160714067

mkdocs_help_doc

MkDocs博客文件命名规则

MkDocs博客文件命名规则

❌ 避免的命名方式

  1. 以点号开头的文件名

  2. ❌ .Net封装ObjectARX自定义实体类型.md

  3. ❌ .gitignore.md

  4. 包含特殊字符的文件名

  5. file@name.md

  6. ❌ file#name.md

  7. ❌ file<>name.md

  8. 过长的文件名

  9. ❌ 超过255字符的文件名

✅ 推荐的命名方式

  1. 使用标准字符

  2. ✅ Net封装ObjectARX自定义实体类型.md

  3. ✅ dotnet-objectarx-custom-entity.md

  4. 使用连字符分隔

  5. ✅ c-sharp-json-processing.md

  6. ✅ wpf-custom-drawer-menu.md

  7. 使用下划线分隔

  8. ✅ objectarx_net_learning_part2.md

mkdocs快速搭建博客

安装依赖

requirement.txt

Text Only
# MkDocs 核心
mkdocs>=1.5.0
mkdocs-material>=9.4.0

# 博客功能(包含在 mkdocs-material 中)
# mkdocs-blog-plugin  # 不需要单独安装

# 功能增强插件
mkdocs-minify-plugin>=0.7.0
mkdocs-glightbox>=0.3.4

# Git 相关插件(可选,需要系统安装 Git)
# mkdocs-git-revision-date-localized-plugin>=1.2.0

# 其他可选插件
mkdocs-awesome-pages-plugin
# mkdocs-redirects
# mkdocs-rss-plugin

yaml文件配置

mkdoc.yml

mkdoc.yml
YAML
site_name: 我的帮助文档
site_description: 关于mkdocs-material支持的markdown语法,包括传统语法和扩展语法
site_author: JerryMa
site_url: http://127.0.0.1:8000

repo_name: 'mkdocsblog'
repo_url: 'https://github.com/mazaiguo/mkdocsblog'
theme:
  name: material
  palette:
    # Toggle light mode
    - scheme: default
      primary: Blue Grey
      accent: Pink
      toggle:
        icon: material/toggle-switch
        name: 切换到明亮模式
    # Toggle dark mode
    - scheme: slate
      primary: blue
      accent: amber
      toggle:
        icon: material/toggle-switch-off-outline
        name: 切换到暗黑模式
  features:
    - announce.dismiss
    - content.tabs.link
    - content.tooltips
    - content.code.copy #代码复制
    - content.code.select
    - content.code.annotate   
    - content.footnote.tooltips
    - header.autohide
    - navigation.footer
    - navigation.indexes
    - navigation.instant
    - navigation.instant.prefetch
    - navigation.instant.progress
    - navigation.prune
    - navigation.sections
    - navigation.tabs
    - navigation.tabs.sticky
    - navigation.top # 返回顶部的按钮 在上滑时出现  
    - navigation.tracking
    - search.highlight # 搜索出的文章关键词加入高亮
    - search.share #搜索分享按钮   
    - search.suggest # 搜索输入一些字母时推荐补全整个单词
    - toc.follow
    - toc.integrate
  language: 'zh'
plugins:
  - macros
  - blog:
      blog_dir: blog
      post_dir: "{blog}/posts"
      post_date_format: full
      post_url_format: "{date}/{slug}"
      pagination_per_page: 10
      pagination_url_format: "page/{page}"
      authors_file: "{blog}/.authors.yml"
      blog_toc: true
      categories_toc: true
      archive: true
      archive_name: 归档
      archive_date_format: "YYYY年MM月"
      archive_url_format: "archive/{date}"
      archive_toc: true
      archive_file: "archive/index.md"
      categories: true
      categories_name: 分类
      categories_url_format: "category/{slug}"
      categories_slugify: !!python/object/apply:pymdownx.slugs.slugify
        kwds:
          case: lower
  - offline
  - tags:
      tags_hierarchy: true
      tags_slugify_format: "tag:{slug}"
      tags_slugify: !!python/object/apply:pymdownx.slugs.slugify
        kwds:
          case: lower
  - search:
      lang: 
        - zh
        - en
      separator: '[\s\-\.]+'
  - minify:
      minify_html: true
      minify_js: true
      minify_css: true
      htmlmin_opts:
        remove_comments: true
      css_files:
        - stylesheets/extra.css
  - glightbox:
      touchNavigation: true
      loop: false
      effect: zoom
      slide_effect: slide
      width: 100%
      height: auto
      zoomable: true
      draggable: true
      skip_classes:
        - custom-skip-class-name
      auto_caption: false
      caption_position: bottom
  # 注释掉 git 插件,因为需要系统安装 Git
  # - git-revision-date-localized:
  #     enable_creation_date: true
  #     type: timeago
  #     locale: zh
  #     fallback_to_build_date: false
  #     exclude:
  #       - index.md
  #       - tags.md
  #       - blog/index.md
extra:
  social:
    - icon: fontawesome/brands/github #联系方式图标 : https://fontawesome.com/ 去这里找图标
      link: https://github.com/mazaiguo
      name: JerryMa on Github
    - icon: fontawesome/brands/gitlab
      link: https://gitlab.zwsoft.cn/mazaiguo
    - icon: fontawesome/regular/envelope
      link: mailto:mazaiguo@126.com
      name: Email
  analytics:
    feedback:
      title: 这个页面对您有帮助吗?
      ratings:
        - icon: material/emoticon-happy-outline
          name: 有帮助
          data: 1
          note: >-
            感谢您的反馈!
        - icon: material/emoticon-sad-outline
          name: 可以改进
          data: 0
          note: >-
            感谢您的反馈!请帮助我们改进这个页面,
            <a href="https://github.com/mazaiguo/mazaiguo.github.io/issues/new/?title=[Feedback]+{title}+-+{url}" target="_blank" rel="noopener">告诉我们需要改进的地方</a>。
  tags:
    HTML5: html
    JavaScript: js
    CSS: css
    Python: python
    AutoCAD: autocad
    C++: cpp
    "Csharp": csharp
    ".NET": dotnet
  generator: false #是否删除页脚显示"使用 MkDocs 材料制造"
#extra_javascript:
  #- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js
  #- javascripts/config.js
extra_css:
  #- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/default.min.css
  - stylesheets/extra.css  
markdown_extensions:
  - abbr
  - admonition
  - attr_list
  - def_list
  - footnotes
  - md_in_html
  - meta
  - toc:
      permalink: true
      title: 目录
  - pymdownx.arithmatex:
      generic: true
  - pymdownx.betterem:
      smart_enable: all
  - pymdownx.caret
  - pymdownx.details
  - pymdownx.emoji:
      emoji_generator: !!python/name:material.extensions.emoji.to_svg
      emoji_index: !!python/name:material.extensions.emoji.twemoji
  - pymdownx.highlight:
      anchor_linenums: true
      line_spans: __span
      pygments_lang_class: true
      linenums: true
      linenums_style: pymdownx.inline
      auto_title: true # 显示编程语言名称
      use_pygments: true
  - pymdownx.inlinehilite
  - pymdownx.keys
  - pymdownx.magiclink:
      normalize_issue_symbols: true
      repo_url_shorthand: true
      user: mazaiguo
      repo: helpdoc
  - pymdownx.mark
  - pymdownx.smartsymbols
  - pymdownx.snippets:
      check_paths: true
  - pymdownx.superfences:
      custom_fences:
        - name: mermaid
          class: mermaid
          format: !!python/name:pymdownx.superfences.fence_code_format
  - pymdownx.tabbed:
      alternate_style: true
      combine_header_slug: true
      slugify: !!python/object/apply:pymdownx.slugs.slugify
        kwds:
          case: lower
  - pymdownx.tasklist:
      custom_checkbox: true
  - pymdownx.tilde
  - pymdownx.critic
copyright: Copyright &copy; 2016 - present [JerryMa](https://github.com/mazaiguo)
nav:
  - 首页: index.md
  - 博客:
     - blog/index.md
  - 归档: archive/index.md
  - 分类: blog/category.md
  - 标签: tags.md
  - 关于: 
     - 关于本站: about.md

增加latex

Bash
1
2
3
$$
\cos x=\sum_{k=0}^{\infty}\frac{(-1)^k}{(2k)!}x^{2k}
$$
\[ \cos x=\sum_{k=0}^{\infty}\frac{(-1)^k}{(2k)!}x^{2k} \]
Bash
1
2
3
The homomorphism $f$ is injective if and only if its kernel is only the
singleton set $e_G$, because otherwise $\exists a,b\in G$ with $a\neq b$ such
that $f(a)=f(b)$.

The homomorphism \(f\) is injective if and only if its kernel is only the singleton set \(e_G\), because otherwise \(\exists a,b\in G\) with \(a\neq b\) such that \(f(a)=f(b)\).

contents tab

  • Sed sagittis eleifend rutrum
  • Donec vitae suscipit est
  • Nulla tempor lobortis orci
  1. Sed sagittis eleifend rutrum
  2. Donec vitae suscipit est
  3. Nulla tempor lobortis orci
C
1
2
3
4
5
6
#include <stdio.h>

int main(void) {
  printf("Hello world!\n");
  return 0;
}
C++
1
2
3
4
5
6
#include <iostream>

int main(void) {
  std::cout << "Hello world!" << std::endl;
  return 0;
}

Example

Example:

Markdown
1
2
3
* Sed sagittis eleifend rutrum
* Donec vitae suscipit est
* Nulla tempor lobortis orci

Result:

  • Sed sagittis eleifend rutrum
  • Donec vitae suscipit est
  • Nulla tempor lobortis orci

Example:

Markdown
1
2
3
1. Sed sagittis eleifend rutrum
2. Donec vitae suscipit est
3. Nulla tempor lobortis orci

Result:

  1. Sed sagittis eleifend rutrum
  2. Donec vitae suscipit est
  3. Nulla tempor lobortis orci

Admonition

Outer Note

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

Inner Note

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

Note

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

Info

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod
nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor
massa, nec semper lorem quam in massa.

Abstract

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod
nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor
massa, nec semper lorem quam in massa.

Info

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod
nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor
massa, nec semper lorem quam in massa.

Tip

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod
nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor
massa, nec semper lorem quam in massa.

Success

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod
nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor
massa, nec semper lorem quam in massa.

Question

这是个问题

Warning

这是个警告

Failure

这是失败的提示

Danger

危险错误的提示

Error

错误的提示

Bug

bug的提示

Quote

quote的提示

特殊数据处理

archive/index.md

tags.md

不识别[TAGS]、[ARCHIVE],用main.py中定义的自定义宏来处理

main.py

main.py
Python
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
"""
MkDocs macros for auto-generating archive content
"""
import os
import re
import yaml
from pathlib import Path
from datetime import datetime, date  # 顶部导入
from collections import defaultdict
import urllib.parse

def generate_url_slug(title):
    """根据实际URL格式生成URL片段"""
    # 1. 转为小写
    slug = title.lower()

    # 2. 特殊处理:如果以点号开头,删除点号并在后面添加分隔符
    if slug.startswith('.'):
        slug = slug[1:]  # 删除开头的点号
        # 在英文和中文之间添加分隔符
        slug = re.sub(r'^([a-z]+)(?=[\u4e00-\u9fff])', r'\1-', slug)

    # 3. 处理其他点号 - 直接删除(保持原有行为)
    slug = slug.replace('.', '')

    # 4. 处理括号(删除括号,保留内容)
    slug = re.sub(r'[()()]', '', slug)

    # 5. 处理连续的+号(C++等)
    slug = re.sub(r'\+{2,}', '', slug)

    # 6. 只对没有自然分隔符的长字符串添加中英文分隔符
    # 检查是否需要添加分隔符(没有空格或连字符的长字符串)
    # if ' ' not in slug and '-' not in slug and len(slug) > 10:
    #     # 在中英文交界处添加分隔符
    #     slug = re.sub(r'(?<=[a-z0-9])(?=[\u4e00-\u9fff])', '-', slug)
    #     slug = re.sub(r'(?<=[\u4e00-\u9fff])(?=[a-z0-9])', '-', slug)

    # 7. 将多个空格合并为一个,然后转为连字符
    slug = re.sub(r'\s+', '-', slug)

    # 8. 清理多余的连字符
    slug = re.sub(r'-+', '-', slug)
    slug = slug.strip('-')

    # 9. URL编码
    return urllib.parse.quote(slug)

def define_env(env):
    """
    Define macros for MkDocs  
    """

    @env.macro  
    def auto_archive():
        """
        Automatically generate archive content from blog posts
        从md文件中完全自动获取所有信息,无硬编码
        """
        try:
            blog_posts_dir = Path("docs/blog/posts")
            if not blog_posts_dir.exists():
                return "## ❌ 错误\n\n无法找到博客文章目录\n\n"

            posts = []
            all_categories = set()  # 收集所有出现的分类
            debug_info = []  # 添加调试信息

            # 遍历所有文章文件
            for md_file in blog_posts_dir.rglob("*.md"):
                debug_info.append(f"处理文件: {md_file}")
                try:
                    with open(md_file, 'r', encoding='utf-8-sig') as f:
                        content = f.read()
                    debug_info.append(f"成功读取: {md_file.name}")

                    # 提取front matter
                    if content.startswith('---'):
                        parts = content.split('---', 2)
                        if len(parts) >= 3:
                            front_matter_text = parts[1].strip()
                            try:
                                # 解析YAML front matter
                                front_matter = yaml.safe_load(front_matter_text)
                                if not front_matter:
                                    continue

                                title = front_matter.get('title', md_file.stem)
                                date_val = front_matter.get('date', '2025-09-18')
                                categories = front_matter.get('categories', [])

                                # 确定主分类 - 完全从front matter获取
                                if isinstance(categories, list) and categories:
                                    main_category = categories[0]
                                    # 收集所有分类
                                    for cat in categories:
                                        all_categories.add(str(cat))
                                else:
                                    # 如果没有分类,跳过这篇文章或使用未分类
                                    main_category = "未分类"
                                    all_categories.add("未分类")

                                # 生成相对路径
                                relative_path = os.path.relpath(md_file, Path("docs/archive")).replace('\\', '/')

                                posts.append({
                                    'title': title,
                                    'date': str(date_val),
                                    'category': str(main_category),
                                    'path': relative_path,
                                    'all_categories': categories if isinstance(categories, list) else [main_category]
                                })

                            except yaml.YAMLError as e:
                                # YAML解析失败,尝试提取基本信息
                                posts.append({
                                    'title': md_file.stem,
                                    'date': '2025-09-18', 
                                    'category': "解析失败",
                                    'path': os.path.relpath(md_file, Path("docs/archive")).replace('\\', '/'),
                                    'all_categories': ["解析失败"]
                                })
                                all_categories.add("解析失败")

                except Exception as e:
                    # 文件读取失败
                    continue

            # 添加调试输出
            debug_text = "\n".join(debug_info[:10])  # 显示前10行调试信息

            if not posts:
                return f"## 📝 调试信息\n\n找到 {len(posts)} 篇文章\n\n调试:\n```\n{debug_text}\n```\n\n分类: {list(all_categories)}\n\n"

            # 生成干净的结果,不包含调试信息

            # 动态生成分类名称映射
            category_display_names = {}
            for category in all_categories:
                cat_lower = category.lower()
                if cat_lower == 'cpp' or 'c++' in cat_lower:
                    category_display_names[category] = 'CPP开发'
                elif cat_lower == 'python' or 'python' in cat_lower:
                    category_display_names[category] = 'Python开发'
                elif 'autocad' in cat_lower or 'cad' in cat_lower:
                    category_display_names[category] = 'AutoCAD开发'
                elif cat_lower == 'csharp' or 'c#' in cat_lower or '.net' in cat_lower:
                    category_display_names[category] = 'C#/.NET开发'
                elif '开发工具' in category or '工具' in category:
                    category_display_names[category] = '开发工具'
                elif '未分类' in category:
                    category_display_names[category] = '未分类'
                elif '解析失败' in category:
                    category_display_names[category] = '解析失败'
                else:
                    # 默认添加"开发"后缀,除非已经包含
                    if '开发' not in category:
                        category_display_names[category] = f'{category}开发'
                    else:
                        category_display_names[category] = category

            # 按日期分组
            posts.sort(key=lambda x: x['date'], reverse=True)
            date_groups = defaultdict(list)

            for post in posts:
                try:
                    if isinstance(post['date'], str):
                        date_obj = datetime.strptime(post['date'], '%Y-%m-%d')
                    else:
                        date_obj = post['date']
                    month_key = date_obj.strftime('%Y年%m月')
                    date_groups[month_key].append(post)
                except:
                    # 日期解析失败,使用默认
                    date_groups['2025年09月'].append(post)

            # 生成归档内容
            result = []

            for month in sorted(date_groups.keys(), reverse=True):
                month_posts = date_groups[month]
                result.append(f"## 🗓️ {month}")
                result.append("")

                # 按分类分组
                category_groups = defaultdict(list)
                for post in month_posts:
                    category_groups[post['category']].append(post)

                # 按分类显示
                for category in sorted(category_groups.keys()):
                    display_name = category_display_names.get(category, category)
                    result.append(f"### {display_name}")

                    for post in category_groups[category]:
                        result.append(f"- [{post['title']}]({post['path']}) - {post['date']}")

                    result.append("")

            # 在最后添加统计信息
            result.append("---")
            result.append("")
            result.append("## 📊 统计信息")
            result.append("")
            result.append(f"- **总文章数**: {len(posts)}篇")
            result.append(f"- **分类数量**: {len(all_categories)}个")
            result.append("- **分类列表**: " + "、".join(sorted(all_categories)))

            return '\n'.join(result)

        except Exception as e:
            return f"## ❌ 生成错误\n\n生成归档时出错: {str(e)}\n\n请检查md文件格式或front matter语法。"

    @env.macro
    def auto_category():
        """
        Automatically generate category content from blog posts
        从md文件中完全自动获取分类信息,无硬编码
        """
        try:
            blog_posts_dir = Path("docs/blog/posts")
            if not blog_posts_dir.exists():
                return "## ❌ 错误\n\n无法找到博客文章目录\n\n"

            # 收集分类信息
            category_info = defaultdict(list)
            all_categories = set()
            nBlogCount = 0
            # 遍历所有文章文件
            for md_file in blog_posts_dir.rglob("*.md"):
                try:
                    with open(md_file, 'r', encoding='utf-8-sig') as f:
                        content = f.read()

                    nBlogCount += 1
                    # 提取front matter
                    if content.startswith('---'):
                        parts = content.split('---', 2)
                        if len(parts) >= 3:
                            front_matter_text = parts[1].strip()
                            try:
                                front_matter = yaml.safe_load(front_matter_text)
                                if not front_matter:
                                    continue

                                title = front_matter.get('title', md_file.stem)
                                categories = front_matter.get('categories', [])
                                date_val = front_matter.get('date', '2025-09-18')

                                # 处理分类
                                if isinstance(categories, list) and categories:
                                    for category in categories:
                                        cat_str = str(category).strip().lower()  # 归一化
                                        all_categories.add(cat_str)
                                        category_info[cat_str].append((title, date_val, md_file.stem))

                            except yaml.YAMLError:
                                continue

                except Exception:
                    continue

            # 生成分类页面内容  
            result = []
            result.append(f"## 🔍 找到{len(all_categories)}个分类")
            result.append("")

            # 编程语言部分
            result.append("## 🖥️ 编程语言")
            result.append("")

            for category in sorted(all_categories):
                cat_lower = category.lower()
                if cat_lower in ['cpp', 'python', 'csharp'] or 'c++' in cat_lower:
                    count = len(category_info[category])

                    if cat_lower == 'cpp' or 'c++' in cat_lower:
                        display_name = 'CPP'
                        icon = '🖥️'
                    elif cat_lower == 'python':
                        display_name = 'Python'
                        icon = '🐍'
                    else:
                        display_name = category
                        icon = '💻'

                    # 添加分类标题
                    result.append(f"### {icon} [{display_name}](category/{cat_lower}.html)")
                    result.append(f"- **文章数量**: {count}篇")

                    latest = []
                    print(f"category={category}, items={category_info[category]}")
                    for t, d, stem in sorted(category_info[category], key=lambda x: x[1], reverse=True)[:3]:
                        url = f"{generate_url_slug(t)}.html"  # 使用title转小写再编码
                        dt = None
                        try:
                            if isinstance(d, datetime):
                                dt = d
                            elif isinstance(d, date):
                                dt = datetime.combine(d, datetime.min.time())
                            elif isinstance(d, str) and len(d) == 10:
                                dt = datetime.strptime(d, "%Y-%m-%d")
                        except Exception as ex:
                            pass
                        if dt:
                            url = f"{dt.year}/{dt.month:02d}/{dt.day:02d}/{generate_url_slug(t)}.html"
                        latest.append(f"[{t}]({url})")
                    result.append(f"- **最新文章**: {', '.join(latest)}")
                    result.append("")
                else:
                    icon = '💻'
                    result.append(f"### {icon} [{cat_lower}](category/{cat_lower}.html)")
                    result.append(f"- **文章数量**: {len(category_info[category])}篇")
                    # 修正这里,生成带链接的最新文章
                    latest = []
                    print(f"category={category}, items={category_info[category]}")
                    for t, d, stem in sorted(category_info[category], key=lambda x: x[1], reverse=True)[:3]:
                        url = f"{generate_url_slug(t)}.html"  # 使用title转小写再编码
                        dt = None
                        try:
                            if isinstance(d, datetime):
                                dt = d
                            elif isinstance(d, date):
                                dt = datetime.combine(d, datetime.min.time())
                            elif isinstance(d, str) and len(d) == 10:
                                dt = datetime.strptime(d, "%Y-%m-%d")
                        except Exception as ex:
                            pass
                        if dt:
                            url = f"{dt.year}/{dt.month:02d}/{dt.day:02d}/{generate_url_slug(t)}.html"
                        latest.append(f"[{t}]({url})")
                    result.append(f"- **最新文章**: {', '.join(latest)}")
                    result.append("")

            # 开发框架和工具部分
            result.append("## 🔧 开发框架与工具")
            result.append("")

            for category in sorted(all_categories):
                cat_lower = category.lower()
                if 'autocad' in cat_lower or 'cad' in cat_lower or '工具' in cat_lower:
                    count = len(category_info[category])

                    # 添加分类标题
                    if 'autocad' in cat_lower or 'cad' in cat_lower:
                        icon = '🏗️'
                        display_name = 'AutoCAD/CAD开发'
                    else:
                        icon = '🔧'
                        display_name = category

                    result.append(f"### {icon} [{display_name}](category/{cat_lower}.html)")
                    result.append(f"- **文章数量**: {count}篇")

                    latest = []
                    print(f"category={category}, items={category_info[category]}")
                    for t, d, stem in sorted(category_info[category], key=lambda x: x[1], reverse=True)[:3]:
                        url = f"{generate_url_slug(t)}.html"  # 使用title转小写再编码
                        dt = None
                        try:
                            if isinstance(d, datetime):
                                dt = d
                            elif isinstance(d, date):
                                dt = datetime.combine(d, datetime.min.time())
                            elif isinstance(d, str) and len(d) == 10:
                                dt = datetime.strptime(d, "%Y-%m-%d")
                        except Exception as ex:
                            pass
                        if dt:
                            url = f"{dt.year}/{dt.month:02d}/{dt.day:02d}/{generate_url_slug(t)}.html"
                        latest.append(f"[{t}]({url})")
                    result.append(f"- **最新文章**: {', '.join(latest)}")
                    result.append("")

            # 统计信息
            result.append("---")
            result.append("")
            result.append("## 📊 分类统计")
            result.append("")
            total_articles = sum(len(articles) for articles in category_info.values())
            result.append(f"- **总分类数**: {len(all_categories)}个")
            result.append(f"- **总文章数**:  {nBlogCount}篇")

            return '\n'.join(result)

        except Exception as e:
            return f"## ❌ 生成错误\n\n{str(e)}\n\n"

    @env.macro
    def auto_tag():
        """
        Automatically generate tag content from blog posts
        从md文件中完全自动获取标签信息,无硬编码
        """

        try:
            all_tags = set()
            tag_info = defaultdict(list)
            blog_posts_dir = Path("docs/blog/posts")
            if not blog_posts_dir.exists():
                return "## ❌ 错误\n\n无法找到博客文章目录\n\n"
            # 遍历所有文章文件
            for md_file in blog_posts_dir.rglob("*.md"):
                try:
                    with open(md_file, 'r', encoding='utf-8-sig') as f:
                        content = f.read()

                    # 提取front matter
                    if content.startswith('---'):
                        parts = content.split('---', 2)
                        if len(parts) >= 3:
                            front_matter_text = parts[1].strip()
                            try:        
                                front_matter = yaml.safe_load(front_matter_text)
                                if not front_matter:
                                    continue

                                tags = front_matter.get('tags', [])

                                # 处理标签
                                if isinstance(tags, list) and tags:
                                    for tag in tags:
                                        tag_str = str(tag)
                                        all_tags.add(tag_str)
                                        tag_info[tag_str].append(md_file.stem)

                            except yaml.YAMLError:
                                continue

                except Exception:
                    continue

            if not tag_info:
                return f"## 📝 调试信息\n\n找到 {len(all_tags)} 个标签,{len(tag_info)} 个有文章的标签\n\n所有标签: {list(all_tags)}\n\n"

            # 生成标签页面内容
            result = []
            result.append(f"## 🔍 找到{len(all_tags)}个标签")
            result.append("")

            for tag in sorted(all_tags):
                count = len(tag_info[tag])
                result.append(f"### [{tag}](tag/{tag}.html)") # 修改为tag/{tag}.html
                result.append(f"- **文章数量**: {count}篇")
                result.append(f"- **最新文章**: {', '.join(tag_info[tag][:3])}")
                result.append("")   
            return '\n'.join(result)

        except Exception as e:
            return f"## ❌ 生成错误\n\n{str(e)}\n\n"

    @env.macro
    def auto_home_category():
        """
        Automatically generate category content from blog posts
        从md文件中完全自动获取分类信息,无硬编码
        """
        try:
            blog_posts_dir = Path("docs/blog/posts")
            if not blog_posts_dir.exists():
                return "## ❌ 错误\n\n无法找到博客文章目录\n\n"

            # 收集分类信息
            category_info = defaultdict(list)
            all_categories = set()
            nBlogCount = 0
            # 遍历所有文章文件
            for md_file in blog_posts_dir.rglob("*.md"):
                try:
                    with open(md_file, 'r', encoding='utf-8-sig') as f:
                        content = f.read()

                    nBlogCount += 1
                    # 提取front matter
                    if content.startswith('---'):
                        parts = content.split('---', 2)
                        if len(parts) >= 3:
                            front_matter_text = parts[1].strip()
                            try:
                                front_matter = yaml.safe_load(front_matter_text)
                                if not front_matter:
                                    continue

                                title = front_matter.get('title', md_file.stem)
                                categories = front_matter.get('categories', [])
                                date_val = front_matter.get('date', '2025-09-18')

                                # 处理分类
                                if isinstance(categories, list) and categories:
                                    for category in categories:
                                        cat_str = str(category).strip().lower()  # 归一化
                                        all_categories.add(cat_str)
                                        category_info[cat_str].append((title, date_val, md_file.stem))

                            except yaml.YAMLError:
                                continue

                except Exception:
                    continue

            # if not category_info:
            #     return f"## 📝 调试信息\n\n找到 {len(all_categories)} 个分类,{len(category_info)} 个有文章的分类\n\n所有分类: {list(all_categories)}\n\n"

            # 生成分类页面内容  
            result = []
            result.append(f"## 🔍 找到{len(all_categories)}个分类")
            result.append("")

            # 编程语言部分
            result.append("## 🖥️ 编程语言")
            result.append("")

            for category in sorted(all_categories):
                cat_lower = category.lower()
                if 'windows' in cat_lower or 'window' in cat_lower:
                    count = len(category_info[category])

                    if cat_lower == 'window' or 'windows' in cat_lower:
                        display_name = 'windows程序'
                        icon = '🔨'
                    else:
                        display_name = category
                        icon = '💻'

                    # 添加分类标题
                    result.append(f"### {icon} [{display_name}](blog/category/{cat_lower}.html)")
                    result.append(f"- **文章数量**: {count}篇")

                    latest = []
                    print(f"category={category}, items={category_info[category]}")
                    for t, d, stem in sorted(category_info[category], key=lambda x: x[1], reverse=True)[:]:
                        url = f"blog/{generate_url_slug(t)}.html"  # 使用title转小写再编码
                        dt = None
                        try:
                            if isinstance(d, datetime):
                                dt = d
                            elif isinstance(d, date):
                                dt = datetime.combine(d, datetime.min.time())
                            elif isinstance(d, str) and len(d) == 10:
                                dt = datetime.strptime(d, "%Y-%m-%d")
                        except Exception as ex:
                            pass
                        if dt:
                            url = f"blog/{dt.year}/{dt.month:02d}/{dt.day:02d}/{generate_url_slug(t)}.html"
                        latest.append(f"<li>[{t}]({url})</li>")
                    result.append(f"- **最新文章**: {' '.join(latest)}")
                    result.append("")
                else:
                    icon = '🛠️'
                    result.append(f"### {icon} [{cat_lower}](blog/category/{cat_lower}.html)")
                    result.append(f"- **文章数量**: {len(category_info[category])}篇")
                    # 修正这里,生成带链接的最新文章
                    latest = []
                    print(f"category={category}, items={category_info[category]}")
                    for t, d, stem in sorted(category_info[category], key=lambda x: x[1], reverse=True)[:]:
                        url = f"blog/{generate_url_slug(t)}.html"  # 使用title转小写再编码
                        dt = None
                        try:
                            if isinstance(d, datetime):
                                dt = d
                            elif isinstance(d, date):
                                dt = datetime.combine(d, datetime.min.time())
                            elif isinstance(d, str) and len(d) == 10:
                                dt = datetime.strptime(d, "%Y-%m-%d")
                        except Exception as ex:
                            pass
                        if dt:
                            url = f"blog/{dt.year}/{dt.month:02d}/{dt.day:02d}/{generate_url_slug(t)}.html"
                        latest.append(f"<li>[{t}]({url})</li>")
                    result.append(f"- **最新文章**: {' '.join(latest)}")
                    result.append("")

            # 开发框架和工具部分
            result.append("## 🏗️ 开发框架与工具")
            result.append("")

            for category in sorted(all_categories):
                cat_lower = category.lower()
                if 'autocad' in cat_lower or 'cad' in cat_lower or '工具' in cat_lower:
                    count = len(category_info[category])

                    # 添加分类标题
                    if 'autocad' in cat_lower or 'cad' in cat_lower:
                        icon = '🏗️'
                        display_name = 'AutoCAD/CAD开发'
                    else:
                        icon = '✏️'
                        display_name = category

                    result.append(f"### {icon} [{display_name}](blog/category/{cat_lower}.html)")
                    result.append(f"- **文章数量**: {count}篇")

                    latest = []
                    print(f"category={category}, items={category_info[category]}")
                    for t, d, stem in sorted(category_info[category], key=lambda x: x[1], reverse=True)[:]:
                        url = f"blog/{generate_url_slug(t)}.html"  # 使用title转小写再编码
                        dt = None
                        try:
                            if isinstance(d, datetime):
                                dt = d
                            elif isinstance(d, date):
                                dt = datetime.combine(d, datetime.min.time())
                            elif isinstance(d, str) and len(d) == 10:
                                dt = datetime.strptime(d, "%Y-%m-%d")
                        except Exception as ex:
                            pass
                        if dt:
                            url = f"blog/{dt.year}/{dt.month:02d}/{dt.day:02d}/{generate_url_slug(t)}.html"
                        latest.append(f"<li>[{t}]({url})</li>")
                    result.append(f"- **最新文章**: {''.join(latest)}")
                    result.append("")

            # 统计信息
            result.append("---")
            result.append("")
            result.append("## 📊 分类统计")
            result.append("")
            total_articles = sum(len(articles) for articles in category_info.values())
            result.append(f"- **总分类数**: {len(all_categories)}个")
            result.append(f"- **总文章数**:  {nBlogCount}篇")

            return '\n'.join(result)

        except Exception as e:
            return f"## ❌ 生成错误\n\n{str(e)}\n\n" 

发布到github中

使用GitHub Actions

使用GitHub Actions可以自动部署网站。在库的根目录下新建一个GitHub Actions workflow,比如:.github/workflows/ci.yml,并粘贴入以下内容:

Material for MkDocs

Text Only
name: ci
on:
  push:
    branches:
      - master
      - main
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v2
        with:
          python-version: 3.x
      - run: pip install mkdocs-material
      - run: mkdocs gh-deploy --force

此时,当一个新的提交推送到mastermain时,我们的静态网站的内容将自动生成并完成部署。可以尝试推送一个提交来查看GitHub Actions的工作状况。

添加相关权限:

image-20250919104024286

image-20250919104720037

yaml-cpp读写yml文件

代码处理

下载地址

Github

主干版rebase到Tag0.8.0

编译版本

创建一个build文件夹,使用默认配置

image-20241227155336653

Debug编译debug版本

Release编译Release版本

使用的时候预处理器中需要填入YAML_CPP_STATIC_DEFINE

读写yaml文件

UTF-8 和 CString 之间的转换函数

你可以定义一些辅助函数,用于在 UTF-8 和 CString(UTF-16)之间进行转换:

C++
#include <atlstr.h> // CString
#include <string>

// UTF-8 -> CString (UTF-16)
CString Utf8ToCString(const std::string& utf8Str) {
    int wideLength = MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, nullptr, 0);
    CString wideStr;
    MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, wideStr.GetBuffer(wideLength), wideLength);
    wideStr.ReleaseBuffer();
    return wideStr;
}

// CString (UTF-16) -> UTF-8
std::string CStringToUtf8(const CString& cstr) {
    int utf8Length = WideCharToMultiByte(CP_UTF8, 0, cstr, -1, nullptr, 0, nullptr, nullptr);
    std::string utf8Str(utf8Length, 0);
    WideCharToMultiByte(CP_UTF8, 0, cstr, -1, &utf8Str[0], utf8Length, nullptr, nullptr);
    return utf8Str;
}

读取 YAML 文件支持 CString

使用 yaml-cpp 读取 YAML 文件时,将读取的 UTF-8 字符串转换为 CString

C++
#include <iostream>
#include <yaml-cpp/yaml.h>
#include <atlstr.h> // CString

// 上述 Utf8ToCString 和 CStringToUtf8 函数应定义在此

int main() {
    try {
        // 加载 YAML 文件
        YAML::Node config = YAML::LoadFile("config.yaml");

        // 读取 UTF-8 编码的字符串,并转换为 CString
        CString name = Utf8ToCString(config["name"].as<std::string>());
        CString description = Utf8ToCString(config["description"].as<std::string>());

        std::wcout << L"Name: " << name.GetString() << std::endl;
        std::wcout << L"Description: " << description.GetString() << std::endl;
    } catch (const YAML::Exception& e) {
        std::cerr << "Error reading YAML file: " << e.what() << std::endl;
    }

    return 0;
}

写入 YAML 文件支持 CString

在写入 YAML 文件时,将 CString 转换为 UTF-8:

C++
#include <iostream>
#include <yaml-cpp/yaml.h>
#include <atlstr.h> // CString

// 上述 Utf8ToCString 和 CStringToUtf8 函数应定义在此

int main() {
    try {
        YAML::Node config;

        // 设置 CString 内容并转换为 UTF-8
        CString name = _T("小明");
        CString description = _T("这是一个包含 Unicode 字符的 YAML 文件。");

        config["name"] = CStringToUtf8(name);
        config["description"] = CStringToUtf8(description);

        // 写入 YAML 文件
        std::ofstream fout("output.yaml");
        fout << config;
        fout.close();

        std::wcout << L"YAML file written successfully!" << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Error writing YAML file: " << e.what() << std::endl;
    }

    return 0;
}

AutoCAD.net Mleader jig

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ZwSoft.ZwCAD.DatabaseServices;
using ZwSoft.ZwCAD.EditorInput;
using ZwSoft.ZwCAD.Geometry;
using ZwSoft.ZwCAD.Runtime;

namespace LeaderPlacement
{
    public class LeaderCmds
    {
        class DirectionalLeaderJig : EntityJig
        {
            private Point3d _start, _end;
            private string _contents;
            private int _index;
            private int _lineIndex;
            private bool _started;
            public DirectionalLeaderJig(string txt, Point3d start, MLeader ld) : base(ld)
            {
                // Store info that's passed in, but don't init the MLeader
                _contents = txt;
                _start = start;
                _end = start;
                _started = false;
            }
            // A fairly standard Sampler function
            protected override SamplerStatus Sampler(JigPrompts prompts)
            {
                var po = new JigPromptPointOptions();
                po.UserInputControls =
                  (UserInputControls.Accept3dCoordinates |
                   UserInputControls.NoNegativeResponseAccepted);
                po.Message = "\nEnd point";
                var res = prompts.AcquirePoint(po);
                if (_end == res.Value)
                {
                    return SamplerStatus.NoChange;
                }
                else if (res.Status == PromptStatus.OK)
                {
                    _end = res.Value;
                    return SamplerStatus.OK;
                }
                return SamplerStatus.Cancel;
            }
            protected override bool Update()
            {
                var ml = (MLeader)Entity;
                if (!_started)
                {
                    if (_start.DistanceTo(_end) > Tolerance.Global.EqualPoint)
                    {
                        // When the jig actually starts - and we have mouse movement -
                        // we create the MText and init the MLeader
                        ml.ContentType = ContentType.MTextContent;
                        var mt = new MText();
                        mt.Contents = _contents;
                        ml.MText = mt;
                        // Create the MLeader cluster and add a line to it
                        _index = ml.AddLeader();
                        _lineIndex = ml.AddLeaderLine(_index);
                        // Set the vertices on the line
                        ml.AddFirstVertex(_lineIndex, _start);
                        ml.AddLastVertex(_lineIndex, _end);
                        // Make sure we don't do this again
                        _started = true;
                    }
                }
                else
                {
                    // We only make the MLeader visible on the second time through
                    // (this also helps avoid some strange geometry flicker)
                    ml.Visible = true;
                    // We already have a line, so just set its last vertex
                    ml.SetLastVertex(_lineIndex, _end);
                }
                if (_started)
                {
                    // Set the direction of the text to depend on the X of the end-point
                    // (i.e. is if to the left or right of the start-point?)
                    var dl = new Vector3d((_end.X >= _start.X ? 1 : -1), 0, 0);
                    ml.SetDogleg(_index, dl);
                }
                return true;
            }
        }

        [CommandMethod("DL")]
        public void DirectionalLeader()
        {
            var doc = ZwSoft.ZwCAD.ApplicationServices.Application.DocumentManager.MdiActiveDocument;
            var ed = doc.Editor;
            var db = doc.Database;
            // Ask the user for the string and the start point of the leader
            var pso = new PromptStringOptions("\nEnter text");
            pso.AllowSpaces = true;
            var pr = ed.GetString(pso);
            if (pr.Status != PromptStatus.OK)
                return;
            var ppr = ed.GetPoint("\nStart point of leader");
            if (ppr.Status != PromptStatus.OK)
                return;
            // Start a transaction, as we'll be jigging a db-resident object
            using (var tr = db.TransactionManager.StartTransaction())
            {
                var bt =
                  (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead, false);
                var btr =
                  (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite, false);
                // Create and pass in an invisible MLeader
                // This helps avoid flickering when we start the jig
                var ml = new MLeader();
                ml.Visible = false;
                // Create jig
                var jig = new DirectionalLeaderJig(pr.StringResult, ppr.Value, ml);
                // Add the MLeader to the drawing: this allows it to be displayed
                btr.AppendEntity(ml);
                tr.AddNewlyCreatedDBObject(ml, true);
                // Set end point in the jig
                var res = ed.Drag(jig);
                // If all is well, commit
                if (res.Status == PromptStatus.OK)
                {
                    tr.Commit();
                }
            }
        }
    }
}

Csharp使用Newtonsoft.Json生成JSON字符串

下载newtonsoftjson

在“解决方案资源管理器”中,右键单击项目,然后选择“管理NuGet程序包”。在NuGet包管理器中,搜索“Newtonsoft.Json”。找到Newtonsoft.Json包,点击安装按钮

C#
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

对于复杂json串

  • 对于简单的json的可以直接解析, 复杂的json, 建议用先创建json对应的类,然后再用JsonConvert.DeserializeObject转为类来解析, 当json比较复杂时, 创建类也比较浪费时间, VS2022为C#提供了json转C#类的工具,先复制需要转为类的json字符串,然后将光标定位到cs文件的空白处,最后点击编辑–选择性粘贴–将Json粘贴为类,如下图:

  • 除了VS自带的工具,也有一些网站提供了类似的功能,例如Json2CSharp

### demo

JSON
1
2
3
4
5
6
7
8
9
{
    "items": [{
        "id": "csrf",
        "attributes": {
            "nonce key": "CSRF NONCE",
            "nonce": "i8Ah1n1DHs71704s2oZnSxmiz4/R3T5mbFrkxErz4m8RUDf3kyX+ror25kZ09Env0tGeVBe+iES8/Y04XRfAKvghp1/+ZIx09oVE7GiE"
        }
    }]
}

class

C#
//如果好用,请收藏地址,帮忙分享。
public class Attributes
{
    /// <summary>
    /// 
    /// </summary>
    public string nonce_key { get; set; }
/// <summary>
/// 
/// </summary>
    public string nonce { get; set; }
}

public class ItemsItem
{
    /// <summary>
    /// 
    /// </summary>
    public string id { get; set; }
    /// <summary>
    /// 
    /// </summary>
    public Attributes attributes { get; set; }
}
//Root可以改成自己喜欢的类名
public class CScrfRoot
{
    /// <summary>
    /// 
    /// </summary>
    public List<ItemsItem> items { get; set; }
}

读取json文件

C#
public static string GetDllDirectory()
{
    string codeBase = Assembly.GetExecutingAssembly().CodeBase;
    UriBuilder uri = new UriBuilder(codeBase);
    string path = Uri.UnescapeDataString(uri.Path);
    return System.IO.Path.GetDirectoryName(path);
}
public MyDropMenu()
{
    System.Uri resourceLocater = new System.Uri("/MyDropMenu;component/ComUseicons.xaml", System.UriKind.Relative);
    ResourceDictionary rd = (ResourceDictionary)Application.LoadComponent(resourceLocater);
    Application.Current.Resources.MergedDictionaries.Add(rd);

    InitializeComponent();
    try
    {
        string jsonFile = GetDllDirectory() + "\\Menu.json";
        // 确保文件存在
        if (!File.Exists(jsonFile))
            throw new FileNotFoundException("The JSON file was not found." + jsonFile);

        // 读取文件内容并反序列化为指定的类型 T
        var reader = new StreamReader(jsonFile);
        var json = reader.ReadToEnd();
        var person = JsonConvert.DeserializeObject<Root>(json);
        Items = person.ItemMenu;
        //遍历items,将icon添加数据
        // 遍历并修改Icon
        foreach (var itemMenu in Items)
        {
            itemMenu.Icon = GetDllDirectory() + "\\config\\Images\\PNG\\" + itemMenu.Icon;
            foreach (var subItem in itemMenu.SubItems)
            {
                subItem.Icon = GetDllDirectory() + "\\config\\Images\\PNG\\" + subItem.Icon;
            }
        }
        LeftMenu.ItemsSource = Items;
    }
    catch (Exception)
    {
        throw;
    }
}

获取token(nonce)值

C#
 public static string getTokenFromJson(string strJson)
        {
            string strRet = "";
            //strJson = "{\"items\":[{\"id\":\"csrf\",\"attributes\":{\"nonce key\":\"CSRF NONCE\",\"nonce\":\"i8Ah1n1DHs71704s2oZnSxmiz4/R3T5mbFrkxErz4m8RUDf3kyX+ror25kZ09Env0tGeVBe+iES8/Y04XRfAKvghp1/+ZIx09oVE7GiE\"}}]}";
            var person = JsonConvert.DeserializeObject<CScrfRoot>(strJson);
            List<ItemsItem> listItems = person.items;
            if(listItems.Count >= 1)
            {
                ItemsItem itemsItem = listItems[0];
                Attributes attr = itemsItem.attributes;
                strRet = attr.nonce;
            }

            return strRet;
        }

LINQ to JSON主要使用到JObject, JArray, JProperty和JValue这四个对象

  • JObject用来生成一个JSON对象,简单来说就是生成”{}”,
  • JArray用来生成一个JSON数组,也就是”[]”,
  • JProperty用来生成一个JSON数据,格式为key/value的值,
  • 而JValue则直接生成一个JSON值
C#
//将json转换为JObject
JObject jObj = new JObject();
jObj.Add("process0id", AdditionClass.GetDeFaultProjectNo());


PdfRow pdfRow1 = new PdfRow();
pdfRow1.status = "success";
pdfRow1.pdfname = "D:\\ZWPDF\\PDF\\JG-72-BL-LB1.pdf";


PdfRow pdfRow2 = new PdfRow();
pdfRow2.status = "error";
pdfRow2.pdfname = "D:\\ZWPDF\\PDF\\JG-72-BL-LB2.pdf";


List<PdfRow> videogames = new List<PdfRow>();
videogames.Add(pdfRow1);
videogames.Add(pdfRow2);

JArray jArray = (JArray)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(videogames));
jObj.Add("message", "转换完成");
jObj.Add("rowPdf", jArray);
Console.WriteLine(jObj.ToString());
JSON
{
  "process0id": "05369",
  "message": "转换完成",
  "rowPdf": [
    {
      "status": "error",
      "pdfname": "D:\\TEST\\ZP-35-DYT-35N3--竖向图框.dwg"
    },
    {
      "status": "error",
      "pdfname": "D:\\TEST\\图框外有多余线条.dwg"
    },
    {
      "status": "error",
      "pdfname": "D:\\TEST\\弧线标注的圆心在图框外1.dwg"
    }
  ]
}

Python应用程序打包指南

使用PyInstaller将Python脚本打包为可执行文件:

Bash
pyinstaller -F -w -i 21.ico gitlog.py

参数说明: - -F: 生成单个可执行文件 - -w: 不显示控制台窗口(适用于GUI应用) - -i 21.ico: 指定应用程序图标 - gitlog.py: 要打包的Python脚本文件

安装docsify

  • 先安装nodejs

  • 安装docsify工具

Bash
npm i docsify-cli -g
  • 初始化目录
Text Only
1
2
3
docsify init <path> [--local false] [--theme vue] [--plugins false]

# docsify i <path> [-l false] [-t vue] [--plugins false]

<path>默认为当前目录。使用./docs(或docs)之类的相对路径。

  • local选项:

    • 速记:-l
    • 类型:布尔值
    • 默认:false
    • 说明:将文件复制docsify到文档路径,默认值是false用来使用cdn.jsdelivr.net<内容分发网络(CDN)>。要显式设置此选项以使用--no-local
  • theme选项:

    • 速记:-t
    • 类型:字符串
    • 默认:vue
    • 说明:选择一个主题,默认为vue,其他选项为bubledarkpure
  • plugins选项:

    • 速记:-p
    • 类型:布尔值
    • 默认:false
    • 描述:提供插件列表作为<script>标签插入到index.html.
Bash
docsify init ./docs
  • serve命令

localhost使用 livereload运行服务器。

Bash
1
2
3
docsify serve <path> [--open false] [--port 3000]

# docsify s <path> [-o false] [-p 3000]
  • open选项:

    • 速记:-o
    • 类型:布尔值
    • 默认:false
    • 说明:在默认浏览器中打开文档,默认为false. 要显式设置此选项以false使用--no-open.
  • port选项:

    • 速记:-p
    • 类型:数字
    • 默认:3000
    • 说明:选择监听端口,默认为3000.
  • Docsify 的生成器。

Bash
1
2
3
docsify generate <path> [--sidebar _sidebar.md]

# docsify g <path> [-s _sidebar.md]
  • sidebar选项:
    • 速记:-s
    • 类型:字符串
    • 默认:_sidebar.md
    • 说明:生成侧边栏文件,默认为_sidebar.md.

插件设置

设置配色

HTML
<script>
    .markdown-section strong {
          color: rgb(239, 112, 96);
        }

    .markdown-section code {
      border-radius: 2px;
      font-family: "Helvetica Neue",Helvetica,"Hiragino Sans GB","Microsoft YaHei",Arial,sans-serif;
      font-size: 16px !important;
      margin: 0 2px;
      padding: 3px 5px;
      white-space: nowrap;
      /*border: 1px solid #282c34;*/
      /*color: rgb(184, 101, 208);*/
    }
    .markdown-section > div > img, .markdown-section pre {
      box-shadow: 2px 2px 20px 6px rgb(255, 255, 255) !important;
    }

    .markdown-section a:not(:hover) {
      text-decoration: none;
    }
    #main h2 span{ color:#18b566 !important; }
    #main h3 span{ color:#089acc !important; }
    #main h4 span{ color:#FF9700 !important; }
    p code{
      background-color: rgb(255, 255, 255) !important;
    }

    /*添加代码块复制按钮样式*/
    .docsify-copy-code-button {
      background: #00a1d6 !important;
      color: #FFFFFF !important;
      font-size: 13px !important;
    }

    ::after{
      color: #9da2fd !important;
      font-size: 13px !important;
    }
    .markdown-section>p {
      font-size: 16px !important;
    }


    /*代码块头部图标 start*/
    .markdown-section pre:before {
      content: '';
      display: block;
      background: url(_media/codeHeader.png);
      height: 30px;
      background-size: 40px;
      background-repeat: no-repeat;
      background-color: #1C1C1C;
      background-position: 10px 10px;
    }
    /*代码块头部图标 end*/

    .markdown-section pre>code {
      color: #c0c3c1;
      font-family: 'Inconsolata', consolas,"PingFang SC", "Microsoft YaHei", monospace;
      background-color: #212121;
      font-size: 15px;
      white-space: pre;
      line-height: 1.5;
      -moz-tab-size: 4;
      -o-tab-size: 4;
      tab-size: 4;
    }

    @media (max-width:600px) {
      pre {
        padding-left: 3px !important;
        padding-right: 3px !important;
        margin-left: -20px !important;
        margin-right: -20px !important;
        box-shadow: 0px 0px 20px 0px #f7f7f7 !important;
      }

      /*代码块复制按钮默认隐藏*/
      .docsify-copy-code-button {
        display: none;
      }

      .advertisement{
        display: none;
      }

    }

    .token.keyword{
      color: #f92672 !important;
    }

    .token.comment{
      color: #75715e !important;
    }

    .token.tag{
      color: #a589ad !important;
    }

    .token.attr-name{
      color: #de916c !important;
    }

    .token.attr-value{
      color: #4faee2 !important;
    }

    .token.macro.property{
      color: #4faee2 !important;
    }

    .token.function{
      color: #66D9EF !important;
    }
    .token.string{
      color: #e6db74 !important;
    }
    .token.punctuation{
      color: #c0c3c1 !important;
    }

    .token.number{
      color: #ae81ff  !important;
    }
    .token.operator{
      color: #f92672 !important;
    }
    .token.builtin{
      color: #66D9EF !important;
    }
    .token.decorator.annotation.punctuation
    {
      color: #a6e22e !important;
    }

    .token.class-name{
      color: #a6e22e !important;
    }

    .token.namespace{
      color: #f92672 !important;
    }

    .token.property{
      color: #f92672 !important;
    }

    .token.parameter{
      color: #f92672 !important;
    }

    .token.variable{
      color: #f92672 !important;
    }

    .token.namespace{
      color: #ededed !important;
    }
</script>
HTML
<script src="assets/js/docsify-copy-code.min.js"></script>
<script src="assets/js/docsify-tabs.min.js"></script>
<script src="assets/js/docsify-themeable.min.js"></script>
<script src="assets/js/prism-line-numbers.min.js"></script>
<script src="assets/js/docsify-sidebar-collapse.min.js"></script>
<script src="assets/js/search.js"></script>
<script src="assets/js/docsify.min.js"></script>
<script src="assets/js/emoji.min.js"></script>
<script src="assets/js/zoom-image.min.js"></script>
<script src="assets/js/prism-autoloader.min.js"></script>
<script src="assets/js/prism-autoloader.js"></script>
<script src="assets/js/prism-javascript.js"></script>
<script src="assets/js/prism-php.js"></script>
<script src="assets/js/prism-bash.js"></script>
<script src="assets/js/prism-c.js"></script>
<script src="assets/js/prism-cpp.js"></script>
<script src="assets/js/prism-python.js"></script>
<script src="assets/js/prism-go.js"></script>
<script src="assets/js/prism-java.js"></script>
<script src="assets/js/prism-sql.js"></script>
<script src="assets/js/prism-markup.js"></script>
<script src="assets/js/prism-yaml.js"></script>
<script src="assets/js/prism-json.js"></script>
<script src="assets/js/prism-docker.js"></script>
<script src="assets/js/prism-git.js"></script>
<script src="assets/js/prism-dart.js"></script>
<script src="assets/js/prism-ini.js"></script>
<script src="assets/js/prism-nginx.js"></script>
<script src="assets/js/prism-css.js"></script>
<script src="assets/js/prism-http.js"></script>
<script src="assets/js/prism-latex.js"></script>
<script src="assets/js/prism-markdown.js"></script>
<script src="assets/js/prism-matlab.js"></script>
<script src="assets/js/prism-powershell.js"></script>
<script src="assets/js/prism-c++.js"></script>
<script src="assets/js/prism-csharp.js"></script>

出现两个搜索框

  • 隐藏第一个input框就好
XML
1
2
3
4
5
<style>
.sidebar .search:nth-child(1){
  display: none;
}
</style>

插件的问题直接去

https://docsify.js.org/#/awesome?id=plugins 查询

美化提示样式

Docsify-alerts

[!NOTE] An alert of type 'note' using global style 'callout'.

[!NOTE|style:flat] An alert of type 'note' using alert specific style 'flat' which overrides global style 'callout'.

As you can see in the second snippet, output can be configured on alert level also. Supported options are listed in following table:

Key Allowed value
style One of follwowing values: callout, flat
label Any text
icon A valid Font Awesome icon, e.g. 'fas fa-comment'
className A name of a CSS class which specifies the look and feel
labelVisibility One of follwowing values: visible (default), hidden
iconVisibility One of follwowing values: visible (default), hidden

[!TIP|style:flat|label:My own heading|iconVisibility:hidden] An alert of type 'tip' using alert specific style 'flat' which overrides global style 'callout'. In addition, this alert uses an own heading and hides specific icon.

As mentioned above you can provide your own alert types. Therefore, you have to provide the type configuration via index.html. Following example shows an additional type COMMENT.

HTML
<script>
  window.$docsify = {
    'flexible-alerts': {
      comment: {
        label: 'Comment',

        // localization
        label: {
          '/en-GB/': 'Comment',
          '/': 'Kommentar'
        },

        // Assuming that we use Font Awesome
        icon: 'fas fa-comment',
        className: 'note'
      }
    }
  };
</script>

[!COMMENT] An alert of type 'comment' using style 'callout' with default settings.

avatar

avatar

docsify-plantuml

docsify-plantuml

HTML
1
2
3
4
5
6
7
8
9
<script>
window.$docsify = {
  plantuml: {
    skin: 'default',
  },
}
</script>

<script src="//unpkg.com/docsify-plantuml/dist/docsify-plantuml.min.js"></script>
  • 怎么用
Text Only
1
2
3
4
5
6
7
8
9
@startuml
autonumber

Alice -> Bob: Authentication Request
Bob --> Alice: Authentication Response

Alice -> Bob: Another authentication Request
Alice <-- Bob: another authentication Response
@enduml
Text Only
1
2
3
4
@startuml
Alice -> Bob: Authentication Request [[$./other-file docs]]
Bob --> Alice: Authentication Response [[$../other-file docs]]
@enduml

徽章查询服务

Bash
1
2
3
[徽章](badgen.net)

[自定义徽章](img.shields.io)

WPF自定义抽屉菜单

处理wpf自定义控件

image-20250901162509475

DropMenuControls\Converters\BooleanToVisibilityConverter

C#
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace DropMenuControls.Converters
{
    public class BooleanToVisibilityConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is bool boolValue)
            {
                return boolValue ? Visibility.Visible : Visibility.Collapsed;
            }
            return Visibility.Collapsed;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is Visibility visibility)
            {
                return visibility == Visibility.Visible;
            }
            return false;
        }
    }
}

DropMenuControls\Models\MenuItem

C#
using System.Collections.Generic;

namespace DropMenuControls.Models
{
    public class MenuData
    {
        public List<MenuItem> ItemMenu { get; set; }
    }

    public class MenuItem
    {
        public string Name { get; set; }
        public string Icon { get; set; }
        public int FontSize { get; set; }
        public string Command { get; set; }
        public List<MenuItem> SubItems { get; set; }

        public bool HasSubItems => SubItems != null && SubItems.Count > 0;
    }
}

Generic.xaml

C#
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:DropMenuControls">

    <!--  DropMenu 控件的默认样式  -->
    <Style TargetType="{x:Type local:DropMenu}">
        <Setter Property="Background" Value="White" />
        <Setter Property="BorderBrush" Value="#DDDDDD" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="FontFamily" Value="Segoe UI" />
        <Setter Property="FontSize" Value="14" />
        <Setter Property="Foreground" Value="#333333" />
        <Setter Property="SnapsToDevicePixels" Value="True" />
        <Setter Property="UseLayoutRounding" Value="True" />
    </Style>

</ResourceDictionary>

DropMenuControls\ViewModels\DropMenuViewModel

C#
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Windows;
using Newtonsoft.Json;
using DropMenuControls.Models;
using System.Reflection;

namespace DropMenuControls.ViewModels
{
    public class DropMenuViewModel : INotifyPropertyChanged
    {
        public ObservableCollection<MenuItemViewModel> MenuItems { get; } = new ObservableCollection<MenuItemViewModel>();

        private MenuItemViewModel _selectedItem;
        public MenuItemViewModel SelectedItem
        {
            get => _selectedItem;
            set
            {
                if (_selectedItem != value)
                {
                    _selectedItem = value;
                    OnPropertyChanged(nameof(SelectedItem));
                    // 移除这里的事件触发,避免重复调用
                    // 事件触发应该只在OnMenuItemClicked中处理
                }
            }
        }

        private string _menuConfigPath = "Menu.json";
        /// <summary>
        /// 菜单配置文件路径
        /// </summary>
        public string MenuConfigPath
        {
            get => _menuConfigPath;
            set
            {
                if (_menuConfigPath != value)
                {
                    _menuConfigPath = value;
                    OnPropertyChanged(nameof(MenuConfigPath));
                }
            }
        }

        private string _iconDirectory = "Icons";
        /// <summary>
        /// 图标目录路径
        /// </summary>
        public string IconDirectory
        {
            get => _iconDirectory;
            set
            {
                if (_iconDirectory != value)
                {
                    _iconDirectory = value;
                    OnPropertyChanged(nameof(IconDirectory));
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public DropMenuViewModel()
        {
            LoadMenuFromJson();
        }

        public DropMenuViewModel(string menuConfigPath, string iconDirectory)
        {
            MenuConfigPath = menuConfigPath;
            IconDirectory = iconDirectory;
            LoadMenuFromJson();
        }

        public void LoadMenuFromJson()
        {
            try
            {
                // 使用配置的路径,支持相对路径和绝对路径
                string jsonPath;
                if (Path.IsPathRooted(MenuConfigPath))
                {
                    // 绝对路径
                    jsonPath = MenuConfigPath;
                }
                else
                {
                    // 相对路径,相对于执行程序目录
                    jsonPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), MenuConfigPath);
                }
                if (!File.Exists(jsonPath))
                {
                    System.Diagnostics.Debug.WriteLine($"菜单配置文件未找到: {jsonPath}");
                    return;
                }

                string jsonContent = File.ReadAllText(jsonPath);

                var settings = new JsonSerializerSettings
                {
                    MissingMemberHandling = MissingMemberHandling.Ignore,
                    NullValueHandling = NullValueHandling.Ignore
                };

                MenuData menuData = JsonConvert.DeserializeObject<MenuData>(jsonContent, settings);

                if (menuData?.ItemMenu != null)
                {
                    BuildMenuItems(menuData.ItemMenu);
                }

            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine($"加载菜单配置失败: {ex.Message}");
            }
        }

        private void BuildMenuItems(System.Collections.Generic.List<MenuItem> menuItems)
        {
            MenuItems.Clear();

            foreach (var item in menuItems)
            {
                var viewModel = new MenuItemViewModel(item, IconDirectory);
                BindMenuItemEvents(viewModel); // 递归绑定所有菜单项的事件
                MenuItems.Add(viewModel);
            }
        }

        // 递归绑定菜单项和所有子项的事件
        private void BindMenuItemEvents(MenuItemViewModel menuItem)
        {
            menuItem.MenuItemClicked += OnMenuItemClicked;

            // 为所有子项也绑定事件
            foreach (var subItem in menuItem.SubItems)
            {
                BindMenuItemEvents(subItem);
            }
        }

        private void OnMenuItemClicked(MenuItemViewModel clickedItem)
        {
            System.Diagnostics.Debug.WriteLine($"[DropMenuViewModel] OnMenuItemClicked被调用: {clickedItem.Name}");

            // 只有当菜单项有命令时才处理选中状态和触发事件
            if (!string.IsNullOrWhiteSpace(clickedItem.Command))
            {
                System.Diagnostics.Debug.WriteLine($"[DropMenuViewModel] 处理有命令的菜单项: {clickedItem.Command}");

                // 清除所有选中状态
                ClearAllSelections();

                // 设置当前选中项
                SelectedItem = clickedItem;
                clickedItem.IsSelected = true;

                // 记录命令并触发外部事件(只触发一次)
                ShowCommandMessage(clickedItem.Command);
                RaiseMenuItemClickedEvent(clickedItem);
            }
            else
            {
                System.Diagnostics.Debug.WriteLine($"[DropMenuViewModel] 跳过无命令的菜单项: {clickedItem.Name}");
            }
        }

        private void ClearAllSelections()
        {
            foreach (var item in MenuItems)
            {
                ClearSelectionRecursive(item);
            }
        }

        // 递归清除所有层级的选中状态
        private void ClearSelectionRecursive(MenuItemViewModel menuItem)
        {
            menuItem.IsSelected = false;

            foreach (var subItem in menuItem.SubItems)
            {
                ClearSelectionRecursive(subItem);
            }
        }

        private void ShowCommandMessage(string command)
        {
            // 不在ViewModel中显示MessageBox,将这个责任交给UI层处理
            // 这样避免了重复弹出对话框的问题
            System.Diagnostics.Debug.WriteLine($"菜单命令被触发: {command}");
        }

        // 公共方法,供外部调用以触发菜单项点击事件
        public event Action<MenuItemViewModel> MenuItemClickedEvent;

        private void RaiseMenuItemClickedEvent(MenuItemViewModel item)
        {
            System.Diagnostics.Debug.WriteLine($"[DropMenuViewModel] 向外部触发MenuItemClickedEvent: {item.Name}");
            MenuItemClickedEvent?.Invoke(item);
        }

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

DropMenuControls\ViewModels\MenuItemViewModel

C#
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Reflection;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using DropMenuControls.Models;

namespace DropMenuControls.ViewModels
{
    public class MenuItemViewModel : INotifyPropertyChanged
    {
        /// <summary>
        /// 图标目录路径
        /// </summary>
        public string IconDirectory { get; set; } = "Icons";

        private string _name;
        private string _icon;
        private int _fontSize;
        private string _command;
        private bool _isExpanded;
        private bool _isSelected;
        private BitmapImage _iconSource;

        public string Name
        {
            get => _name;
            set
            {
                if (_name != value)
                {
                    _name = value;
                    OnPropertyChanged(nameof(Name));
                }
            }
        }

        public string Icon
        {
            get => _icon;
            set
            {
                if (_icon != value)
                {
                    _icon = value;
                    OnPropertyChanged(nameof(Icon));
                    LoadIcon();
                }
            }
        }

        public int FontSize
        {
            get => _fontSize;
            set
            {
                if (_fontSize != value)
                {
                    _fontSize = value;
                    OnPropertyChanged(nameof(FontSize));
                }
            }
        }

        public string Command
        {
            get => _command;
            set
            {
                if (_command != value)
                {
                    _command = value;
                    OnPropertyChanged(nameof(Command));
                }
            }
        }

        public bool IsExpanded
        {
            get => _isExpanded;
            set
            {
                if (_isExpanded != value)
                {
                    _isExpanded = value;
                    OnPropertyChanged(nameof(IsExpanded));
                }
            }
        }

        public bool IsSelected
        {
            get => _isSelected;
            set
            {
                if (_isSelected != value)
                {
                    _isSelected = value;
                    OnPropertyChanged(nameof(IsSelected));
                }
            }
        }

        public BitmapImage IconSource
        {
            get => _iconSource;
            set
            {
                if (_iconSource != value)
                {
                    _iconSource = value;
                    OnPropertyChanged(nameof(IconSource));
                }
            }
        }

        public ObservableCollection<MenuItemViewModel> SubItems { get; } = new ObservableCollection<MenuItemViewModel>();

        public bool HasSubItems => SubItems.Count > 0;

        private void LoadIcon()
        {
            try
            {
                if (!string.IsNullOrWhiteSpace(Icon))
                {
                    string iconPath;
                    if (Path.IsPathRooted(IconDirectory))
                    {
                        // IconDirectory是绝对路径
                        iconPath = Path.Combine(IconDirectory, Icon);
                    }
                    else
                    {
                        // IconDirectory是相对路径,相对于执行程序目录
                        iconPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), IconDirectory, Icon);
                    }
                    if (File.Exists(iconPath))
                    {
                        var bitmap = new BitmapImage();
                        bitmap.BeginInit();
                        bitmap.UriSource = new Uri(iconPath, UriKind.Absolute);
                        bitmap.DecodePixelWidth = 24;
                        bitmap.DecodePixelHeight = 24;
                        bitmap.EndInit();
                        IconSource = bitmap;
                    }
                }
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine($"加载图标失败: {ex.Message}");
            }
        }

        public ICommand ExecuteMenuItemCommand { get; }
        public event Action<MenuItemViewModel> MenuItemClicked;
        public event PropertyChangedEventHandler PropertyChanged;

        public MenuItemViewModel()
        {
            ExecuteMenuItemCommand = new RelayCommand(ExecuteMenuItem);
        }

        public MenuItemViewModel(MenuItem menuItem, string iconDirectory = "Icons")
        {
            ExecuteMenuItemCommand = new RelayCommand(ExecuteMenuItem);
            Name = menuItem.Name;
            Icon = menuItem.Icon;
            FontSize = menuItem.FontSize;
            Command = menuItem.Command;
            IconDirectory = iconDirectory;
            LoadIcon();

            if (menuItem.SubItems != null)
            {
                foreach (var subItem in menuItem.SubItems)
                {
                    var subViewModel = new MenuItemViewModel(subItem, iconDirectory);
                    SubItems.Add(subViewModel);
                }
            }

            OnPropertyChanged(nameof(HasSubItems));
        }

        private DateTime _lastExecutionTime = DateTime.MinValue;
        private static readonly TimeSpan MinimumInterval = TimeSpan.FromMilliseconds(500); // 500ms防重复间隔

        private void ExecuteMenuItem()
        {
            var currentTime = DateTime.Now;

            // 防重复触发机制 - 使用时间间隔控制
            if (currentTime - _lastExecutionTime < MinimumInterval)
            {
                System.Diagnostics.Debug.WriteLine($"[MenuItemViewModel] 防止重复执行 (间隔太短): {Name}, 间隔: {(currentTime - _lastExecutionTime).TotalMilliseconds}ms");
                return;
            }

            _lastExecutionTime = currentTime;
            System.Diagnostics.Debug.WriteLine($"[MenuItemViewModel] 执行菜单项: {Name}");

            // 如果有子项,切换展开状态
            if (HasSubItems)
            {
                IsExpanded = !IsExpanded;
            }

            // 总是触发事件,让DropMenuViewModel来处理逻辑(包括选中状态和命令执行)
            MenuItemClicked?.Invoke(this);
        }

        // 清除选中状态(用于实现单选逻辑)
        public void ClearSelection()
        {
            IsSelected = false;
            foreach (var subItem in SubItems)
            {
                subItem.ClearSelection();
            }
        }

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    // Csharp 7.3 兼容的RelayCommand实现
    public class RelayCommand : ICommand
    {
        private readonly Action _execute;
        private readonly Func<bool> _canExecute;

        public RelayCommand(Action execute, Func<bool> canExecute = null)
        {
            _execute = execute ?? throw new ArgumentNullException(nameof(execute));
            _canExecute = canExecute;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter)
        {
            return _canExecute?.Invoke() ?? true;
        }

        public void Execute(object parameter)
        {
            _execute();
        }
    }
}

DropMenu.xaml

C#
<UserControl
    x:Class="DropMenuControls.DropMenu"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:converters="clr-namespace:DropMenuControls.Converters"
    xmlns:vm="clr-namespace:DropMenuControls.ViewModels">

    <UserControl.Resources>
        <converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />

        <!--  主菜单按钮样式 - 现代化动态效果  -->
        <Style x:Key="MenuButtonStyle" TargetType="Button">
            <Setter Property="Background" Value="#F8F9FA" />
            <Setter Property="BorderBrush" Value="#E0E0E0" />
            <Setter Property="BorderThickness" Value="1" />
            <Setter Property="Padding" Value="15,12" />
            <Setter Property="Margin" Value="5,2" />
            <Setter Property="HorizontalContentAlignment" Value="Left" />
            <Setter Property="Cursor" Value="Hand" />
            <Setter Property="FontSize" Value="14" />
            <Setter Property="FontWeight" Value="Medium" />
            <Setter Property="RenderTransformOrigin" Value="0.5,0.5" />
            <Setter Property="Effect">
                <Setter.Value>
                    <DropShadowEffect
                        BlurRadius="0"
                        Opacity="0"
                        ShadowDepth="0"
                        Color="Black" />
                </Setter.Value>
            </Setter>
            <Setter Property="RenderTransform">
                <Setter.Value>
                    <TransformGroup>
                        <ScaleTransform ScaleX="1" ScaleY="1" />
                        <TranslateTransform X="0" Y="0" />
                    </TransformGroup>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsSelected}" Value="True">
                    <Setter Property="Background" Value="#2196F3" />
                    <Setter Property="Foreground" Value="White" />
                    <Setter Property="BorderBrush" Value="#1976D2" />
                    <Setter Property="FontWeight" Value="Bold" />
                    <DataTrigger.EnterActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[0].ScaleX"
                                    To="1.03"
                                    Duration="0:0:0.25" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[0].ScaleY"
                                    To="1.03"
                                    Duration="0:0:0.25" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="Effect.BlurRadius"
                                    To="8"
                                    Duration="0:0:0.25" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="Effect.Opacity"
                                    To="0.4"
                                    Duration="0:0:0.25" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="Effect.ShadowDepth"
                                    To="2"
                                    Duration="0:0:0.25" />
                            </Storyboard>
                        </BeginStoryboard>
                    </DataTrigger.EnterActions>
                    <DataTrigger.ExitActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[0].ScaleX"
                                    To="1"
                                    Duration="0:0:0.25" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[0].ScaleY"
                                    To="1"
                                    Duration="0:0:0.25" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="Effect.BlurRadius"
                                    To="0"
                                    Duration="0:0:0.25" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="Effect.Opacity"
                                    To="0"
                                    Duration="0:0:0.25" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="Effect.ShadowDepth"
                                    To="0"
                                    Duration="0:0:0.25" />
                            </Storyboard>
                        </BeginStoryboard>
                    </DataTrigger.ExitActions>
                </DataTrigger>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter Property="Background" Value="#E8F4FD" />
                    <Setter Property="BorderBrush" Value="#2196F3" />
                    <Trigger.EnterActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[1].Y"
                                    To="-1"
                                    Duration="0:0:0.2" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[0].ScaleX"
                                    To="1.01"
                                    Duration="0:0:0.2" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[0].ScaleY"
                                    To="1.01"
                                    Duration="0:0:0.2" />
                            </Storyboard>
                        </BeginStoryboard>
                    </Trigger.EnterActions>
                    <Trigger.ExitActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[1].Y"
                                    To="0"
                                    Duration="0:0:0.2" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[0].ScaleX"
                                    To="1"
                                    Duration="0:0:0.2" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[0].ScaleY"
                                    To="1"
                                    Duration="0:0:0.2" />
                            </Storyboard>
                        </BeginStoryboard>
                    </Trigger.ExitActions>
                </Trigger>
                <Trigger Property="IsPressed" Value="True">
                    <Trigger.EnterActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[0].ScaleX"
                                    To="0.97"
                                    Duration="0:0:0.1" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[0].ScaleY"
                                    To="0.97"
                                    Duration="0:0:0.1" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[1].Y"
                                    To="1"
                                    Duration="0:0:0.1" />
                            </Storyboard>
                        </BeginStoryboard>
                    </Trigger.EnterActions>
                    <Trigger.ExitActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[0].ScaleX"
                                    To="1"
                                    Duration="0:0:0.15" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[0].ScaleY"
                                    To="1"
                                    Duration="0:0:0.15" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[1].Y"
                                    To="0"
                                    Duration="0:0:0.15" />
                            </Storyboard>
                        </BeginStoryboard>
                    </Trigger.ExitActions>
                </Trigger>
            </Style.Triggers>
        </Style>

        <!--  子菜单项样式  -->
        <Style x:Key="SubMenuItemStyle" TargetType="Border">
            <Setter Property="Background" Value="Transparent" />
            <Setter Property="BorderBrush" Value="#E0E0E0" />
            <Setter Property="BorderThickness" Value="1" />
            <Setter Property="Padding" Value="25,8" />
            <Setter Property="Margin" Value="8,1" />
            <Setter Property="Cursor" Value="Hand" />
            <Setter Property="CornerRadius" Value="4" />
            <Setter Property="RenderTransformOrigin" Value="0.5,0.5" />
            <Setter Property="RenderTransform">
                <Setter.Value>
                    <TransformGroup>
                        <TranslateTransform X="0" />
                        <ScaleTransform ScaleX="1" ScaleY="1" />
                    </TransformGroup>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsSelected}" Value="True">
                    <Setter Property="Background" Value="#43A047" />
                    <Setter Property="BorderBrush" Value="#4CAF50" />
                    <Setter Property="BorderThickness" Value="2" />
                </DataTrigger>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter Property="Background" Value="#E8F5E8" />
                    <Setter Property="BorderBrush" Value="#4CAF50" />
                    <Trigger.EnterActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[0].X"
                                    To="5"
                                    Duration="0:0:0.15" />
                            </Storyboard>
                        </BeginStoryboard>
                    </Trigger.EnterActions>
                    <Trigger.ExitActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Children[0].X"
                                    To="0"
                                    Duration="0:0:0.15" />
                            </Storyboard>
                        </BeginStoryboard>
                    </Trigger.ExitActions>
                </Trigger>
            </Style.Triggers>
        </Style>

        <!--  子菜单按钮样式 - 最简版本,完全稳定  -->
        <Style x:Key="SubMenuButtonStyle" TargetType="Button">
            <Setter Property="Background" Value="Transparent" />
            <Setter Property="BorderBrush" Value="#E0E0E0" />
            <Setter Property="BorderThickness" Value="1" />
            <Setter Property="Padding" Value="25,8" />
            <Setter Property="Margin" Value="8,1" />
            <Setter Property="Cursor" Value="Hand" />
            <Setter Property="HorizontalContentAlignment" Value="Left" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Border
                            Padding="{TemplateBinding Padding}"
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            CornerRadius="4">
                            <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsSelected}" Value="True">
                    <Setter Property="Background" Value="#43A047" />
                    <Setter Property="BorderBrush" Value="#4CAF50" />
                    <Setter Property="Foreground" Value="White" />
                    <Setter Property="BorderThickness" Value="2" />
                </DataTrigger>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter Property="Background" Value="#E8F5E8" />
                    <Setter Property="BorderBrush" Value="#4CAF50" />
                </Trigger>
            </Style.Triggers>
        </Style>

        <!--  菜单项数据模板  -->
        <DataTemplate x:Key="MenuItemTemplate" DataType="{x:Type vm:MenuItemViewModel}">
            <StackPanel>
                <!--  主菜单项按钮  -->
                <Button Command="{Binding ExecuteMenuItemCommand}" Style="{StaticResource MenuButtonStyle}">
                    <StackPanel Orientation="Horizontal">
                        <!--  图标  -->
                        <Image
                            Width="24"
                            Height="24"
                            Margin="0,0,10,0"
                            Source="{Binding IconSource}" />

                        <!--  文本  -->
                        <TextBlock
                            VerticalAlignment="Center"
                            FontSize="{Binding FontSize}"
                            Text="{Binding Name}" />

                        <!--  展开指示器 - 简化版本  -->
                        <TextBlock
                            Margin="5,0,0,0"
                            VerticalAlignment="Center"
                            RenderTransformOrigin="0.5,0.5"
                            Text="▼"
                            Visibility="{Binding HasSubItems, Converter={StaticResource BooleanToVisibilityConverter}}">
                            <TextBlock.RenderTransform>
                                <RotateTransform Angle="0" />
                            </TextBlock.RenderTransform>
                            <TextBlock.Style>
                                <Style TargetType="TextBlock">
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding IsExpanded}" Value="True">
                                            <DataTrigger.EnterActions>
                                                <BeginStoryboard>
                                                    <Storyboard>
                                                        <DoubleAnimation
                                                            Storyboard.TargetProperty="RenderTransform.Angle"
                                                            To="180"
                                                            Duration="0:0:0.2" />
                                                    </Storyboard>
                                                </BeginStoryboard>
                                            </DataTrigger.EnterActions>
                                            <DataTrigger.ExitActions>
                                                <BeginStoryboard>
                                                    <Storyboard>
                                                        <DoubleAnimation
                                                            Storyboard.TargetProperty="RenderTransform.Angle"
                                                            To="0"
                                                            Duration="0:0:0.2" />
                                                    </Storyboard>
                                                </BeginStoryboard>
                                            </DataTrigger.ExitActions>
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </TextBlock.Style>
                        </TextBlock>
                    </StackPanel>
                </Button>
                <!--  子菜单项容器 - 简化版本  -->
                <StackPanel
                    Margin="15,0,0,0"
                    RenderTransformOrigin="0.5,0"
                    Visibility="{Binding IsExpanded, Converter={StaticResource BooleanToVisibilityConverter}}">
                    <StackPanel.RenderTransform>
                        <ScaleTransform ScaleY="1" />
                    </StackPanel.RenderTransform>
                    <StackPanel.Style>
                        <Style TargetType="StackPanel">
                            <Setter Property="Opacity" Value="0" />
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding IsExpanded}" Value="True">
                                    <DataTrigger.EnterActions>
                                        <BeginStoryboard>
                                            <Storyboard>
                                                <DoubleAnimation
                                                    Storyboard.TargetProperty="Opacity"
                                                    To="1"
                                                    Duration="0:0:0.25" />
                                                <DoubleAnimation
                                                    Storyboard.TargetProperty="RenderTransform.ScaleY"
                                                    From="0"
                                                    To="1"
                                                    Duration="0:0:0.25" />
                                            </Storyboard>
                                        </BeginStoryboard>
                                    </DataTrigger.EnterActions>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </StackPanel.Style>

                    <ItemsControl ItemsSource="{Binding SubItems}">
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <!--  改用Button统一处理点击事件,避免重复绑定  -->
                                <Button Command="{Binding ExecuteMenuItemCommand}" Style="{StaticResource SubMenuButtonStyle}">
                                    <StackPanel Orientation="Horizontal">
                                        <Image
                                            Width="20"
                                            Height="20"
                                            Margin="0,0,8,0"
                                            Source="{Binding IconSource}" />

                                        <TextBlock FontSize="{Binding FontSize}" Text="{Binding Name}" />
                                    </StackPanel>
                                </Button>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                    </ItemsControl>
                </StackPanel>
            </StackPanel>
        </DataTemplate>
    </UserControl.Resources>

    <ScrollViewer VerticalScrollBarVisibility="Auto">
        <ItemsControl
            Background="White"
            ItemTemplate="{StaticResource MenuItemTemplate}"
            ItemsSource="{Binding MenuItems}" />
    </ScrollViewer>
</UserControl>

DropMenu.cs

C#
using System;
using System.Windows;
using System.Windows.Controls;
using DropMenuControls.ViewModels;

namespace DropMenuControls
{
    /// <summary>
    /// 高级自定义下拉菜单控件
    /// 支持丰富的视觉效果、动画和交互体验
    /// 使用MVVM模式架构
    /// </summary>
    public partial class DropMenu : UserControl
    {
        public DropMenuViewModel ViewModel { get; private set; }

        // 依赖属性 - 菜单配置文件路径
        public static readonly DependencyProperty MenuConfigPathProperty =
            DependencyProperty.Register("MenuConfigPath", typeof(string), typeof(DropMenu),
                new PropertyMetadata("Menu.json", OnMenuConfigPathChanged));

        // 依赖属性 - 图标目录路径
        public static readonly DependencyProperty IconDirectoryProperty =
            DependencyProperty.Register("IconDirectory", typeof(string), typeof(DropMenu),
                new PropertyMetadata("Icons", OnIconDirectoryChanged));

        /// <summary>
        /// 菜单配置文件路径
        /// </summary>
        public string MenuConfigPath
        {
            get { return (string)GetValue(MenuConfigPathProperty); }
            set { SetValue(MenuConfigPathProperty, value); }
        }

        /// <summary>
        /// 图标目录路径
        /// </summary>
        public string IconDirectory
        {
            get { return (string)GetValue(IconDirectoryProperty); }
            set { SetValue(IconDirectoryProperty, value); }
        }

        /// <summary>
        /// 菜单项被点击事件
        /// </summary>
        public event EventHandler<MenuItemClickedEventArgs> MenuItemClicked;

        public DropMenu()
        {
            InitializeComponent();

            // 创建ViewModel并设置DataContext,传入配置路径
            ViewModel = new DropMenuViewModel(MenuConfigPath, IconDirectory);
            DataContext = ViewModel;

            // 订阅ViewModel的事件
            ViewModel.MenuItemClickedEvent += OnMenuItemClicked;
        }

        private void OnMenuItemClicked(MenuItemViewModel clickedItem)
        {
            // 触发控件的外部事件
            MenuItemClicked?.Invoke(this, new MenuItemClickedEventArgs(clickedItem));
        }

        // 依赖属性变化处理
        private static void OnMenuConfigPathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is DropMenu control && e.NewValue is string newPath)
            {
                // 更新ViewModel的配置路径
                if (control.ViewModel != null)
                {
                    control.ViewModel.MenuConfigPath = newPath;
                    control.ViewModel.LoadMenuFromJson();
                }
            }
        }

        private static void OnIconDirectoryChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is DropMenu control && e.NewValue is string newIconDirectory)
            {
                // 更新ViewModel的图标目录
                if (control.ViewModel != null)
                {
                    control.ViewModel.IconDirectory = newIconDirectory;
                    control.ViewModel.LoadMenuFromJson(); // 重新加载以应用新的图标路径
                }
            }
        }

        /// <summary>
        /// 公共方法:重新加载菜单
        /// </summary>
        public void ReloadMenu()
        {
            ViewModel?.LoadMenuFromJson();
        }

        /// <summary>
        /// 公共方法:清除所有选中状态
        /// </summary>
        public void ClearSelection()
        {
            if (ViewModel != null)
            {
                foreach (var item in ViewModel.MenuItems)
                {
                    item.ClearSelection();
                }
            }
        }
    }

    /// <summary>
    /// 菜单项点击事件参数
    /// </summary>
    public class MenuItemClickedEventArgs : EventArgs
    {
        public MenuItemViewModel ClickedItem { get; }
        public string Command { get; }
        public string ItemName { get; }

        public MenuItemClickedEventArgs(MenuItemViewModel clickedItem)
        {
            ClickedItem = clickedItem;
            Command = clickedItem?.Command;
            ItemName = clickedItem?.Name;
        }
    }
}

pachages.config

C#
1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Newtonsoft.Json" version="13.0.3" targetFramework="net47" />
</packages>

在ZWCAD中使用

将菜单设定到PalletSet中

Commands.cs

C#
 public class Commands
 {
     internal static PaletteSet ps = null;
     [CommandMethod("TEST1")]
     public void TEST1()
     {
         if (ps == null)
         {
             ps = new PaletteSet("抽屉菜单");
             ps.Add("Menu", new PaletteBridge());
             ps.Visible = true;
             ps.Dock = ZwSoft.ZwCAD.Windows.DockSides.Left;
             if (ps.Size.Width < 200)
             {
                 ps.Size = new System.Drawing.Size(500, ps.Size.Height);
             }
         }
         else
             ps.Visible = !ps.Visible;
     }
     private static PaletteSet _palette;
     [CommandMethod("ShowDockWpf")]
     public void ShowDockWpf()
     {
         if (_palette == null)
         {
             _palette = new PaletteSet("我的 WPF Dock 面板")
             {
                 Style = PaletteSetStyles.ShowPropertiesMenu |
                     PaletteSetStyles.ShowAutoHideButton |
                     PaletteSetStyles.ShowCloseButton
             };

             _palette.Size = new System.Drawing.Size(300, 400);

             // WPF 控件
             var wpfCtrl = new WpfDropMenu();

             // 通过 ElementHost 封装
             var host = new ElementHost
             {
                 Dock = System.Windows.Forms.DockStyle.Fill,
                 Child = wpfCtrl
             };

             _palette.Add("工具面板", host);
         }

         _palette.Visible = true; // 显示
     }
 }

WpfDropMenu.xaml

C#
<UserControl
    x:Class="ZwObjectZrxNet3.Views.WpfDropMenu"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:controls="clr-namespace:DropMenuControls;assembly=DropMenuControls"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:ZwObjectZrxNet3.Views"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">
    <Grid>
        <controls:DropMenu x:Name="dropMenu" MenuItemClicked="CustomDropMenu_MenuItemClicked" />
    </Grid>
</UserControl>

WpfDropMenu.cs

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using ZwSoft.ZwCAD.Windows.Data;
using ZwSoft.ZwCAD.ApplicationServices;
using System.Reflection;
using System.IO;
using Path = System.IO.Path;

namespace ZwObjectZrxNet3.Views
{
    /// <summary>
    /// WpfDropMenu.xaml 的交互逻辑
    /// </summary>
    public partial class WpfDropMenu : UserControl
    {
        public WpfDropMenu()
        {
            InitializeComponent();
            string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            dropMenu.IconDirectory = Path.Combine(path, "Icons");
            dropMenu.MenuConfigPath = Path.Combine(path, "Menu.json");
        }

        private void CustomDropMenu_MenuItemClicked(object sender, DropMenuControls.MenuItemClickedEventArgs args)
        {
            string itemName = args.ItemName ?? "未知项目";
            string command = args.Command ?? "无命令";
            using (DocumentLock loc = ZwSoft.ZwCAD.ApplicationServices.Application.DocumentManager.MdiActiveDocument.LockDocument())
            {
                Document doc = ZwSoft.ZwCAD.ApplicationServices.Application.DocumentManager.MdiActiveDocument;
                if (!string.IsNullOrWhiteSpace(command) && command != "无命令")
                {
                    string testCommand = command;
                    //如果command没有/n 则加上
                    if (!command.StartsWith(" "))
                    {
                        testCommand = command + " ";
                    }

                    doc.SendStringToExecute(testCommand, false, false, false);
                    //MessageBox.Show($"执行命令: {command}",
                    //    "菜单项点击", MessageBoxButton.OK, MessageBoxImage.Information);
                }
            }
        }
    }
}

创建一个Winform用户控件做中间层转接WPF控件

PaletteBridge.cs

C#
namespace ZwObjectZrxNet3
{
    partial class PaletteBridge
    {
        /// <summary> 
        /// 必需的设计器变量。
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary> 
        /// 清理所有正在使用的资源。
        /// </summary>
        /// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region 组件设计器生成的代码

        /// <summary> 
        /// 设计器支持所需的方法 - 不要修改
        /// 使用代码编辑器修改此方法的内容。
        /// </summary>
        private void InitializeComponent()
        {
            this.elementHost1 = new System.Windows.Forms.Integration.ElementHost();
            this.wpfDropMenu1 = new Views.WpfDropMenu();
            this.SuspendLayout();
            // 
            // elementHost1
            // 
            this.elementHost1.Dock = System.Windows.Forms.DockStyle.Fill;
            this.elementHost1.Location = new System.Drawing.Point(0, 0);
            this.elementHost1.Name = "elementHost1";
            this.elementHost1.Size = new System.Drawing.Size(150, 150);
            this.elementHost1.TabIndex = 0;
            this.elementHost1.Text = "elementHost1";
            this.elementHost1.Child = wpfDropMenu1;
            // 
            // PallateBridge
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.Controls.Add(this.elementHost1);
            this.Name = "PallateBridge";
            this.ResumeLayout(false);

        }

        #endregion

        private System.Windows.Forms.Integration.ElementHost elementHost1;
        private Views.WpfDropMenu wpfDropMenu1;
    }
}

image-20250901163639624

Menu.json

JSON
{
    "ItemMenu": [
        {
            "Name":"Home",
            "Icon":"Home.png",
            "FontSize":16,
            "Command": "Home",
            "SubItems":[]
        },
        {
            "Name": "Customer",
            "Icon": "BootstrapIcons-_1Circle.png",
            "FontSize":16,
            "SubItems": [
                {
                    "Name": "Customer1",
                    "Icon": "notification 22.png",
                    "FontSize":16,
                    "Command": "Test1"
                },
                {
                    "Name": "Customer2",
                    "Icon": "ET 28.png",
                    "FontSize":16,
                    "Command": "Test2"
                },
                {
                    "Name": "Customer3",
                    "Icon": "email 33.png",
                    "FontSize":16,
                    "Command": "Test3"
                },
                {
                    "Name": "Customer4",
                    "Icon": "edit 13.png",
                    "FontSize":16,
                    "Command": "Test4"
                },
                {
                    "Name": "Customer5",
                    "Icon": "credits 6.png",
                    "FontSize":16,
                    "Command": "Test5"
                }
            ]
        },
        {
            "Name": "Providers",
            "Icon": "BootstrapIcons-_2Circle.png",
            "FontSize":16,
            "SubItems":[
                {
                    "Name": "Provider1",
                    "Icon": "phonecall 24.png",
                    "FontSize":16,
                    "Command": "Test1"
                },
                {
                    "Name": "Provider2",
                    "Icon": "cart 11.png",
                    "FontSize":16,
                    "Command": "Test2"
                },
                {
                    "Name": "Provider3",
                    "Icon": "calculator 31.png",
                    "FontSize":16,
                    "Command": "Test3"
                }
            ]
        },
        {
            "Name": "Others",
            "Icon": "BootstrapIcons-_3Circle.png",
            "FontSize":16,
            "SubItems":[
                {
                    "Name": "Other1",
                    "Icon": "social_server_fault 81.png",
                    "FontSize":16,
                    "Command": "Test1"
                },
                {
                    "Name": "Other2",
                    "Icon": "check 43.png",
                    "FontSize":16,
                    "Command": "Test2"
                }
            ]
        }
    ]
}