<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>SlimeNull</title><description>Blogs</description><link>https://slimenull.com/</link><language>zh_CN</language><item><title>如何使用 ADB 从安卓设备中提取 Boot 镜像.md</title><link>https://slimenull.com/posts/extract_boot_img_with_adb_from_android_device/</link><guid isPermaLink="true">https://slimenull.com/posts/extract_boot_img_with_adb_from_android_device/</guid><description>刷入 Lineage OS 的 recovery, 然后从 recovery 中启用 ADB, 再执行 adb shell dd, 最后 adb pull 就可以了</description><pubDate>Thu, 15 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;网上下载手机机型以及操作系统对应的 boot.img, 以及从手机官网下载线刷包并提取 boot.img 的方式此文章不再赘述.&lt;/p&gt;
&lt;p&gt;本文只说通过电脑执行 adb 来提取 boot 镜像的方案. 严格来讲, 只需要你的手机能在 recovery 中启用 ADB 就可以实现.&lt;/p&gt;
&lt;h2&gt;在 Recovery 中启用 ADB&lt;/h2&gt;
&lt;p&gt;重启进入 recovery (方案不再赘述), 然后尝试在 recovery 中寻找 &quot;启用 ADB&quot; 的选项. 如果你的手机 recovery 中有这个东西, 跳过此部分.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;大部分国内厂商手机都不会给这个功能, 如果你的手机也没有, 那么接下来我们刷入并使用 &lt;a href=&quot;https://lineageos.org/&quot;&gt;Lineage OS&lt;/a&gt; 提供的 recovery.&lt;/p&gt;
&lt;p&gt;在&lt;a href=&quot;https://wiki.lineageos.org/devices/&quot;&gt;Lineage OS 设备列表&lt;/a&gt;中寻找你的设备, 然后只下载对应的 recovery.img 即可. 注意, 不需要下载别的.&lt;/p&gt;
&lt;p&gt;现在, 重启到 bootloader, 然后使用 fastboot 指令刷入 recovery&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fastboot flash recovery recovery.img
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意, 此操作需要你的设备已经解了 BL 锁, 否则指令无法正常执行&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;确认你现在的设备是 A 槽还是 B 槽&lt;/h2&gt;
&lt;p&gt;现如今大多数设备都区分 AB 分区, 也就是说, 诸如 boot, system 这些分区, 都有 AB 两份. 名称也像 boot_a boot_b 这样.&lt;/p&gt;
&lt;p&gt;重启进入 bootloader, 执行以下指令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fastboot getvar current-slot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你会得到下面这样的消息:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;current-slot: b
Finished. Total time: 0.238s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在, 我们知道当前槽位了&lt;/p&gt;
&lt;h2&gt;提取 boot 分区&lt;/h2&gt;
&lt;p&gt;重启进入 recovery, 并且启用 ADB.&lt;/p&gt;
&lt;p&gt;对于 Lineage OS 的 Recovery, &quot;Enable ADB&quot; 的选项一般会在 &quot;Advanced&quot; 当中.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/lineage_os_recovery_advanced.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在, 在你的电脑上执行 &lt;code&gt;adb shell dd&lt;/code&gt; 提取镜像, 注意, &lt;strong&gt;如果你是 A 槽, 那么 if 后面的 boot_b 就应该是 boot_a&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;adb shell dd if=/dev/block/bootdevice/by-name/boot_b of=/sdcard/boot.img
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果执行成功了, 你会得到类似这样的消息:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;196608+0 records in
196608+0 records out
100663296 bytes (96 M) copied, 0.096 s, 0.9 G/s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果失败了, 你会得到类似这样的消息:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dd: /dev/block/bootdevice/by-name/boot: No such file or directory
0+0 records in
0+0 records out
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;在成功后, 我们只需要通过 &lt;code&gt;adb pull&lt;/code&gt; 就可以从设备上下载镜像到电脑了.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;adb pull /sdcard/boot.img
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此指令会将 boot.img 下载到你终端的当前工作目录.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意, 你必须将它通过 adb pull 下载下来, 因为 recovery 中的 /sdcard 分区是临时的, 再重启之后, 你就看不到刚刚通过 dd 弄出来的 boot.img 了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;后续操作&lt;/h2&gt;
&lt;p&gt;如果你用它是为了使用 Magisk, APatch 这类工具进行修补, 那么下一步, 你可以通过 &lt;code&gt;adb push&lt;/code&gt; 将它上传到手机中.&lt;/p&gt;
&lt;p&gt;重启手机, 然后执行 adb 指令就可以了:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;adb push boot.img /sdcard/boot.img
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>JSON 高级序列化: 循环引用、多态、接口代理、自动构造、转换、集合填充</title><link>https://slimenull.com/posts/advancedjsonserialization/</link><guid isPermaLink="true">https://slimenull.com/posts/advancedjsonserialization/</guid><description>基于反射和动态代理将复杂的, 带循环引用的, 多态的数据序列化与反序列化. 通过自定义转换器, 实现几乎任意数据类型的序列化与反序列化.</description><pubDate>Fri, 26 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;你的领导突然要你做跨进程的插件开发, 但项目已经有一套 API 抽象了,
你不希望重新再抽一套, 于是决定将原有进程内插件的复杂数据抽象, 序列化成 JSON, 再通过命名管道发送到另一个进程.
但问题是, 这套数据是复杂的, 带循环引用的, 多态的, 基于接口的. 并且你不希望为了跨进程通信再写一次接口的实现类.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文基于 System.Text.Json, 并且需要使用 Castle.Core 中的动态代理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;本文解决的是这类问题:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象之间存在互相引用, 不能因为循环引用导致序列化失败&lt;/li&gt;
&lt;li&gt;成员类型是接口, 反序列化时没有现成实现类可以 new&lt;/li&gt;
&lt;li&gt;对象运行时类型比声明类型更具体, 序列化时不能丢失派生类型成员&lt;/li&gt;
&lt;li&gt;某些值类型只有 &lt;code&gt;Parse(string)&lt;/code&gt;, 不想为每个类型单独写转换器&lt;/li&gt;
&lt;li&gt;某些类型没有无参构造函数, 或者集合属性只读, 但反序列化时仍然希望还原数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简单说, 我们不是要做一个普通的 DTO JSON 序列化, 而是要尽量把一张复杂的对象图保存下来, 之后再还原成能够继续使用的对象图.&lt;/p&gt;
&lt;h2&gt;如何解决循环引用&lt;/h2&gt;
&lt;p&gt;假如你有两个类型:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A { public B Other { get; set; } }
class B { public A Other { get; set; } }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它们明显互相引用, 但你希望能够正确的序列化它, 那么使用 System.Text.Json 中的 ReferenceHandler.Preserve 可以轻易解决这个需求.&lt;/p&gt;
&lt;p&gt;在 JsonSerializerOptions 中指定 ReferenceHandler, 你就可以直接跑通下面明显带有循环引用的对象序列化了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System.Text.Json;
using System.Text.Json.Serialization;

var a = new A();
var b = new B();
a.Other = b;
b.Other = a;

var jsonSerializerOptions = new JsonSerializerOptions()
{
    ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve
};

var json = JsonSerializer.Serialize(a, jsonSerializerOptions);
Console.WriteLine(json);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它将输出:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;$id&quot;:&quot;1&quot;,&quot;Other&quot;:{&quot;$id&quot;:&quot;2&quot;,&quot;Other&quot;:{&quot;$ref&quot;:&quot;1&quot;}}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然, 你也可以将它重新序列化到 C# 对象, 对象引用关系仍然存在:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var objRoot = JsonSerializer.Deserialize&amp;lt;A&amp;gt;(json, jsonSerializerOptions);
Console.WriteLine(objRoot.GetHashCode());
Console.WriteLine(objRoot.Other.Other.GetHashCode());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于两个对象是同一个对象, 上述两个 &lt;code&gt;Console.WriteLine&lt;/code&gt; 输出的哈希值是相同的.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ReferenceHandler&lt;/code&gt; 是一个抽象类, 它包含一个 &lt;code&gt;CreateResolver&lt;/code&gt; 方法, 用来创建一个实际存储容器, 它用来添加引用或从存储中取得引用.&lt;/p&gt;
&lt;p&gt;当 JSON 反序列化的时候, 遇到 &lt;code&gt;$id&lt;/code&gt; 节点, 它就会将它加入到 &lt;code&gt;ReferenceResolver&lt;/code&gt; 中, 在后续遇到 &lt;code&gt;$ref&lt;/code&gt; 节点时, 再从中取出.&lt;/p&gt;
&lt;h2&gt;自定义 ReferenceHandler 以保证多个序列化调用使用同一引用存储&lt;/h2&gt;
&lt;p&gt;JSON 反序列化的时候, 如果我们使用了自定义的转换器, 即便当前对象是前面对象的引用, System.Text.Json 也会调用我们的转换器.&lt;/p&gt;
&lt;p&gt;这会导致在我们在自定义转换器中使用 &lt;code&gt;Utf8JsonReader&lt;/code&gt; 读取到的节点只是一个带有 &lt;code&gt;$ref&lt;/code&gt; 属性的空节点, 但在转换器中, 我们还没办法拿到 &lt;code&gt;ReferenceResolver&lt;/code&gt; 来手动解析对象引用.
并且, 即便我们不手动通过 &lt;code&gt;reader&lt;/code&gt; 解析, 而是在转换器内继续调用 &lt;code&gt;JsonSerializer.Deserialize&lt;/code&gt; 并传入原有 &lt;code&gt;reader&lt;/code&gt;, &lt;code&gt;typeToConvert&lt;/code&gt; 和 &lt;code&gt;options&lt;/code&gt;, 由于这是两次调用, 它也没办法成功解析引用.&lt;/p&gt;
&lt;p&gt;所以: 我们需要自己定义一个 &lt;code&gt;ReferenceHandler&lt;/code&gt;, 以做到:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使其在多次 &lt;code&gt;Deserialize&lt;/code&gt; 调用中能够共享引用解析存储&lt;/li&gt;
&lt;li&gt;能够让我们自己的转换器解析引用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以, 我们需要创建一个和 &lt;code&gt;ReferenceHandler.Preserve&lt;/code&gt; 行为一致的, 但是多次调用 &lt;code&gt;CreateResolver&lt;/code&gt; 时只返回同一个对象的实现. 并且为了避免反序列化的时候拿到 &quot;上一次反序列化&quot; 时加入的引用, 它还应当有 &lt;code&gt;Reset&lt;/code&gt; 方法用于重置存储.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;文档参考: &lt;a href=&quot;https://learn.microsoft.com/zh-cn/dotnet/standard/serialization/system-text-json/preserve-references&quot;&gt;如何在 System.Text.Json 中保留引用并处理或忽略循环引用&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;class PreserveReferenceResolver : ReferenceResolver
{
    private uint _referenceCount;
    private readonly Dictionary&amp;lt;string, object&amp;gt; _referenceIdToObjectMap = [];
    private readonly Dictionary&amp;lt;object, string&amp;gt; _objectToReferenceIdMap = new (ReferenceEqualityComparer.Instance);

    public override void AddReference(string referenceId, object value)
    {
        if (!_referenceIdToObjectMap.TryAdd(referenceId, value))
        {
            throw new JsonException();
        }
    }

    public override string GetReference(object value, out bool alreadyExists)
    {
        if (_objectToReferenceIdMap.TryGetValue(value, out string? referenceId))
        {
            alreadyExists = true;
        }
        else
        {
            _referenceCount++;
            referenceId = _referenceCount.ToString();
            _objectToReferenceIdMap.Add(value, referenceId);
            alreadyExists = false;
        }

        return referenceId;
    }

    public override object ResolveReference(string referenceId)
    {
        if (!_referenceIdToObjectMap.TryGetValue(referenceId, out object? value))
        {
            throw new JsonException();
        }

        return value;
    }
}

class SingletonReferenceHandler : ReferenceHandler
{
    public SingletonReferenceHandler() =&amp;gt; Reset();
    private ReferenceResolver? _rootedResolver;
    public override ReferenceResolver CreateResolver() =&amp;gt; _rootedResolver!;
    public void Reset() =&amp;gt; _rootedResolver = new PreserveReferenceResolver();

    public static void AddReferenceIsNecessary(JsonObject jsonObject, object value, JsonSerializerOptions options)
    {
        if (jsonObject.TryGetPropertyValue(&quot;$id&quot;, out var propertyValueNode) &amp;amp;&amp;amp;
            propertyValueNode is not null &amp;amp;&amp;amp;
            propertyValueNode.GetValueKind() == JsonValueKind.String)
        {
            if (options?.ReferenceHandler is SingletonReferenceHandler referenceHandler)
            {
                referenceHandler.CreateResolver().AddReference(propertyValueNode.GetValue&amp;lt;string&amp;gt;(), value);
            }
            else
            {
                throw new InvalidOperationException(&quot;Can not add reference&quot;);
            }
        }
    }

    public static bool TryResolveReference(JsonObject jsonObject, JsonSerializerOptions options, [NotNullWhen(true)] out object? value)
    {
        if (jsonObject.TryGetPropertyValue(&quot;$ref&quot;, out var propertyValueNode) &amp;amp;&amp;amp;
            propertyValueNode is not null &amp;amp;&amp;amp;
            propertyValueNode.GetValueKind() == JsonValueKind.String)
        {
            if (options?.ReferenceHandler is SingletonReferenceHandler referenceHandler)
            {
                value = referenceHandler.CreateResolver().ResolveReference(propertyValueNode.GetValue&amp;lt;string&amp;gt;());
                return true;
            }
            else
            {
                throw new InvalidOperationException(&quot;Can not resolve reference&quot;);
            }
        }

        value = null;
        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在转换器内调用 &lt;code&gt;Serialize&lt;/code&gt; / &lt;code&gt;Deserialize&lt;/code&gt; 的时候, 它们共享引用, 就不会出现 id 冲突或者无法解析引用的情况了&lt;/li&gt;
&lt;li&gt;在转换器内添加引用或解引用的时候, 调用上面写的静态方法即可.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用时建议把这个 &lt;code&gt;SingletonReferenceHandler&lt;/code&gt; 当成一次序列化或反序列化流程的上下文对象, 每次开始前都调用一次 &lt;code&gt;Reset&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var referenceHandler = new SingletonReferenceHandler();

var options = new JsonSerializerOptions()
{
    ReferenceHandler = referenceHandler,
    WriteIndented = true
};

referenceHandler.Reset();
var json = JsonSerializer.Serialize(rootObject, options);

referenceHandler.Reset();
var restored = JsonSerializer.Deserialize&amp;lt;RootObject&amp;gt;(json, options);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有一个容易踩坑的地方: &lt;code&gt;JsonSerializerOptions&lt;/code&gt; 可以复用, 但引用解析器中的对象映射不能跨业务请求复用. 如果不 &lt;code&gt;Reset&lt;/code&gt;, 下一次反序列化可能会拿到上一次 JSON 中注册过的对象, 结果就非常玄学.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当然, 如果你每次序列化或反序列化时都创建新的 Options 对象, 并且每次都创建新的 ReferenceHandler 对象, 那上面说的问题也就不存在了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;值得一提的是, 之所以我们不直接在 &lt;code&gt;SingletonReferenceHandler&lt;/code&gt; 中使用 &lt;code&gt;ReferenceHandler.Preserve.CreateResolver&lt;/code&gt; 而是自己写一个 resolver 并实例化它,
是因为库自带的 Preserve 实现的 &lt;code&gt;CreateResolver&lt;/code&gt; 会直接丢给你一个 &lt;code&gt;InvalidOperation&lt;/code&gt; 异常. 库内部专门判断了 &quot;这个类型是不是我内置的 Handler&quot;, 如果是, 它会调用一个 internal 方法.&lt;/p&gt;
&lt;p&gt;但既然是 internal, 我们自然无法调用或实现了. 而刚刚提到的 &lt;code&gt;CreateResolver&lt;/code&gt; 实现, 内部实现就是一句 &lt;code&gt;throw new InvalidOperationException()&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;基于动态代理, 在无需实体类的情况下反序列化接口&lt;/h2&gt;
&lt;p&gt;接下来, 我们需要继续创建一个转换器, 转换器的逻辑是创建一个动态代理对象用来表示接口反序列化的对象. 而代理执行的逻辑, 则是通过从 &lt;code&gt;reader&lt;/code&gt; 中读出的 &lt;code&gt;JsonNode&lt;/code&gt;, 继续调用 &lt;code&gt;Deserialize&lt;/code&gt; 反序列化接口的成员.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class InterfaceProxyConverter : JsonConverter&amp;lt;object&amp;gt;
{
    public List&amp;lt;Type&amp;gt; TypesToProxy { get; } = new List&amp;lt;Type&amp;gt;();
    public List&amp;lt;Assembly&amp;gt; TypeLookupAssemblies { get; } = new();

    public override bool CanConvert(Type typeToConvert)
    {
        if (!typeToConvert.IsInterface)
        {
            return false;
        }

        foreach (var typeToProxy in TypesToProxy)
        {
            if (typeToProxy.IsAssignableFrom(typeToConvert))
            {
                return true;
            }
        }

        return false;
    }

    private class InterfaceInterceptor : IInterceptor
    {
        private readonly Type _targetInterface;
        private readonly JsonObject _payload;
        private readonly JsonSerializerOptions _deserializeOptions;

        private readonly Dictionary&amp;lt;string, object?&amp;gt; _propertyCache = new Dictionary&amp;lt;string, object?&amp;gt;();

        public InterfaceInterceptor(Type targetInterface, JsonObject payload, JsonSerializerOptions deserializeOptions)
        {
            _targetInterface = targetInterface;
            _payload = payload;
            _deserializeOptions = deserializeOptions;
        }

        private static bool IsPropertyGetInvocation(IInvocation invocation, out string propertyName)
        {
            propertyName = null!;

            var method = invocation.Method;

            if (!method.IsSpecialName ||
                !method.Name.StartsWith(&quot;get_&quot;, StringComparison.Ordinal) ||
                method.GetParameters().Length != 0 ||
                method.ReturnType == typeof(void))
            {
                return false;
            }

            propertyName = method.Name.Substring(&quot;get_&quot;.Length);
            return propertyName.Length &amp;gt; 0;
        }

        private object? VisitProperty(string propertyName, Type propertyType)
        {
            if (_propertyCache.TryGetValue(propertyName, out var cachedValue))
            {
                return cachedValue;
            }

            if (!_payload.TryGetPropertyValue(propertyName, out var propertyValueNode))
            {
                return null;
            }

            var createdValue = JsonSerializer.Deserialize(propertyValueNode, propertyType, _deserializeOptions);
            _propertyCache[propertyName] = createdValue;

            return createdValue;
        }

        public void WallAllProperties()
        {
            var interfaceProperties = _targetInterface.GetInterfaces()
                .Append(_targetInterface)
                .SelectMany(inter =&amp;gt; inter.GetProperties())
                .DistinctBy(prop =&amp;gt; prop.Name)
                .ToArray();

            foreach (var property in interfaceProperties)
            {
                VisitProperty(property.Name, property.PropertyType);
            }
        }

        public void Intercept(IInvocation invocation)
        {
            if (!IsPropertyGetInvocation(invocation, out var propertyName))
            {
                return;
            }

            invocation.ReturnValue = VisitProperty(propertyName, invocation.Method.ReturnType);
        }
    }

    public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.Null)
        {
            return null;
        }

        var node = JsonNode.Parse(ref reader);

        if (node is not JsonObject payload)
        {
            throw new JsonException($&quot;Expected JSON object for interface type &apos;{typeToConvert}&apos;.&quot;);
        }

        if (SingletonReferenceHandler.TryResolveReference(payload, options, out var referenceValue))
        {
            return referenceValue;
        }

        if (payload.TryGetPropertyValue(&quot;$type&quot;, out var typeJson) &amp;amp;&amp;amp;
            typeJson is not null &amp;amp;&amp;amp;
            typeJson.GetValueKind() == JsonValueKind.String)
        {
            typeToConvert = TypeLookupAssemblies
                .Select(asm =&amp;gt; asm.GetType(typeJson.GetValue&amp;lt;string&amp;gt;()))
                .Where(t =&amp;gt; t is not null)
                .First()!;
        }

        var proxyGenerator = new ProxyGenerator();
        var interceptor = new InterfaceInterceptor(typeToConvert, payload, options);
        var proxy = proxyGenerator.CreateInterfaceProxyWithoutTarget(typeToConvert, interceptor);
        SingletonReferenceHandler.AddReferenceIsNecessary(payload, proxy, options);

        interceptor.WallAllProperties();

        return proxy;
    }

    public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
    {
        throw new NotSupportedException();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个转换器的核心思路是:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;CanConvert&lt;/code&gt; 只处理接口类型, 并且要求这个接口属于 &lt;code&gt;TypesToProxy&lt;/code&gt; 指定的接口体系&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Read&lt;/code&gt; 先把当前 JSON 对象读成 &lt;code&gt;JsonObject&lt;/code&gt;, 这样后续可以按属性名取值&lt;/li&gt;
&lt;li&gt;如果 JSON 里是 &lt;code&gt;{ &quot;$ref&quot;: &quot;...&quot; }&lt;/code&gt;, 就直接从 &lt;code&gt;SingletonReferenceHandler&lt;/code&gt; 中解析已有对象&lt;/li&gt;
&lt;li&gt;如果 JSON 里有 &lt;code&gt;$type&lt;/code&gt;, 就把目标接口切换成 &lt;code&gt;$type&lt;/code&gt; 指向的接口&lt;/li&gt;
&lt;li&gt;使用 Castle DynamicProxy 创建一个没有实际目标对象的接口代理&lt;/li&gt;
&lt;li&gt;属性 getter 被调用时, 代理从 JSON 中取出对应属性节点, 再用 &lt;code&gt;JsonSerializer.Deserialize&lt;/code&gt; 递归还原属性值&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;WallAllProperties&lt;/code&gt; 看起来有些奇怪, 但它很重要. 它会在代理创建后主动访问所有属性, 让子对象尽早反序列化出来. 这样当对象图里存在 A 引用 B, B 又引用 A 时, 引用表中能尽快注册完整对象, 后续 &lt;code&gt;$ref&lt;/code&gt; 才能正确解析.&lt;/p&gt;
&lt;p&gt;举个接口反序列化的例子:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface INode
{
    string Name { get; }
    INode? Parent { get; }
    IReadOnlyList&amp;lt;INode&amp;gt; Children { get; }
}

public interface IFolderNode : INode
{
    string Path { get; }
}

var referenceHandler = new SingletonReferenceHandler();

var interfaceProxyConverter = new InterfaceProxyConverter();
interfaceProxyConverter.TypesToProxy.Add(typeof(INode));
interfaceProxyConverter.TypeLookupAssemblies.Add(typeof(INode).Assembly);

var options = new JsonSerializerOptions()
{
    ReferenceHandler = referenceHandler,
    WriteIndented = true,
    Converters =
    {
        interfaceProxyConverter
    }
};

var json = &quot;&quot;&quot;
{
  &quot;$id&quot;: &quot;1&quot;,
  &quot;$type&quot;: &quot;IFolderNode&quot;,
  &quot;Name&quot;: &quot;root&quot;,
  &quot;Path&quot;: &quot;/&quot;,
  &quot;Parent&quot;: null,
  &quot;Children&quot;: {
    &quot;$id&quot;: &quot;2&quot;,
    &quot;$values&quot;: [
      {
        &quot;$id&quot;: &quot;3&quot;,
        &quot;$type&quot;: &quot;IFolderNode&quot;,
        &quot;Name&quot;: &quot;child&quot;,
        &quot;Path&quot;: &quot;/child&quot;,
        &quot;Parent&quot;: { &quot;$ref&quot;: &quot;1&quot; },
        &quot;Children&quot;: {
          &quot;$id&quot;: &quot;4&quot;,
          &quot;$values&quot;: []
        }
      }
    ]
  }
}
&quot;&quot;&quot;;

referenceHandler.Reset();
var node = JsonSerializer.Deserialize&amp;lt;INode&amp;gt;(json, options)!;

Console.WriteLine(node.Name);                       // root
Console.WriteLine(node.Children[0].Name);           // child
Console.WriteLine(node.Children[0].Parent == node); // True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意, 上面的 &lt;code&gt;$type&lt;/code&gt; 写的是接口名, 所以 &lt;code&gt;TypeLookupAssemblies&lt;/code&gt; 中必须能找到这个接口. 如果类型有命名空间, 需要写完整名称, 例如 &lt;code&gt;Demo.IFolderNode&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;大量自定义类型的自定义转换&lt;/h2&gt;
&lt;p&gt;你的项目中可能定义了大量的基本数据类型, 例如 double2, int3x3 这类表示向量或矩阵的类型. 但幸运的是, 它们都有静态的 &lt;code&gt;Parse&lt;/code&gt; 方法.&lt;/p&gt;
&lt;p&gt;非常好, 这样我们就可以用一个转换器基于反射解决它们的序列化和反序列化了.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class CommonParseConverter : JsonConverter&amp;lt;object&amp;gt;
{
    private static readonly Type[] _requiredParseMethodParameters = new[] { typeof(string) };
    private readonly Dictionary&amp;lt;Type, MethodInfo?&amp;gt; _parseMethods = new Dictionary&amp;lt;Type, MethodInfo?&amp;gt;();

    private static MethodInfo? FindParseMethod(Type type)
        =&amp;gt; type.GetMethod(&quot;Parse&quot;, BindingFlags.Public | BindingFlags.Static, null, _requiredParseMethodParameters, null);

    public override bool CanConvert(Type typeToConvert)
    {
        if (Nullable.GetUnderlyingType(typeToConvert) is { } nullableActualType)
        {
            typeToConvert = nullableActualType;
        }

        if (typeToConvert == typeof(bool) ||
            typeToConvert == typeof(decimal) ||
            typeToConvert == typeof(string) ||
            typeToConvert == typeof(DateTime) ||
            typeToConvert == typeof(DateTimeOffset) ||
            typeToConvert == typeof(Guid) ||
            typeToConvert == typeof(TimeSpan) ||
            typeToConvert == typeof(Uri) ||
            typeToConvert == typeof(Version) ||
            typeToConvert.IsEnum ||
            typeToConvert.IsPrimitive)
        {
            return false;
        }

        if (!_parseMethods.TryGetValue(typeToConvert, out var parseMethod))
        {
            _parseMethods[typeToConvert] = parseMethod = FindParseMethod(typeToConvert);
        }

        return parseMethod is not null;
    }

    public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.Null)
        {
            return default;
        }

        if (reader.TokenType != JsonTokenType.String)
        {
            throw new JsonException($&quot;Expected string token, but got {reader.TokenType}.&quot;);
        }

        if (Nullable.GetUnderlyingType(typeToConvert) is { } nullableActualType)
        {
            typeToConvert = nullableActualType;
        }

        var text = reader.GetString()!;
        return _parseMethods[typeToConvert]!.Invoke(null, new object[] { text });
    }

    public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
    {
        if (value is null)
        {
            writer.WriteNullValue();
            return;
        }

        if (value is IConvertible convertible)
        {
            writer.WriteStringValue(convertible.ToString(CultureInfo.InvariantCulture));
        }
        else
        {
            writer.WriteStringValue(value.ToString());
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的转换器支持任意带有静态 Parse(string) 的类型.&lt;/p&gt;
&lt;p&gt;例如:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public readonly struct Double2
{
    public double X { get; }
    public double Y { get; }

    public Double2(double x, double y)
    {
        X = x;
        Y = y;
    }

    public static Double2 Parse(string text)
    {
        var parts = text.Split(&apos;,&apos;);
        return new Double2(
            double.Parse(parts[0], CultureInfo.InvariantCulture),
            double.Parse(parts[1], CultureInfo.InvariantCulture));
    }

    public override string ToString()
        =&amp;gt; $&quot;{X.ToString(CultureInfo.InvariantCulture)},{Y.ToString(CultureInfo.InvariantCulture)}&quot;;
}

public class TransformInfo
{
    public Double2 Position { get; set; }
}

var options = new JsonSerializerOptions()
{
    Converters =
    {
        new CommonParseConverter()
    }
};

var obj = JsonSerializer.Deserialize&amp;lt;TransformInfo&amp;gt;(
    &quot;&quot;&quot;{ &quot;Position&quot;: &quot;10.5,20.25&quot; }&quot;&quot;&quot;,
    options)!;

Console.WriteLine(obj.Position.X); // 10.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它的原理很直接: &lt;code&gt;CanConvert&lt;/code&gt; 通过反射查找目标类型是否有公开静态 &lt;code&gt;Parse(string)&lt;/code&gt; 方法, 有就让这个转换器接管. 读取 JSON 时要求当前 token 是字符串, 然后调用 &lt;code&gt;Parse&lt;/code&gt;. 写入 JSON 时则调用 &lt;code&gt;ToString&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;这里要注意两点:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;ToString&lt;/code&gt; 和 &lt;code&gt;Parse&lt;/code&gt; 的格式必须互相匹配, 否则序列化后就反序列化不回去了&lt;/li&gt;
&lt;li&gt;最好使用 &lt;code&gt;CultureInfo.InvariantCulture&lt;/code&gt;, 不要让小数点格式受系统区域设置影响&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;自动处理类型和接口多态&lt;/h2&gt;
&lt;p&gt;System.Text.Json 原生支持多态序列化和反序列化, 但是要求你往目标基类上添加特性标记&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[JsonPolymorphic(TypeDiscriminatorPropertyName = &quot;$discriminator&quot;)]
[JsonDerivedType(typeof(ThreeDimensionalPoint), typeDiscriminator: &quot;3d&quot;)]
public class BasePoint { }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用自定义类型转换器, 自己写入类型名称到 &lt;code&gt;$type&lt;/code&gt; 属性, 并且反序列化的时候自己读, 就可以实现在不添加特性的情况下自动处理多态了.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AutoPolymorphicConverter&amp;lt;T&amp;gt; : JsonConverter&amp;lt;T&amp;gt;
    where T : class
{
    public bool IsForInterface { get; set; }
    public List&amp;lt;Assembly&amp;gt; TypeLookupAssemblies { get; } = new();

    public override bool CanConvert(Type typeToConvert)
        =&amp;gt; typeToConvert == typeof(T);

    private static Type FindMaxInterface(Type type)
    {
        var maxInterface = typeof(T);
        var typeInterfaces = type.GetInterfaces();

        bool continueIterate = true;
        while (continueIterate)
        {
            continueIterate = false;
            foreach (var typeInterface in typeInterfaces)
            {
                if (typeInterface != maxInterface &amp;amp;&amp;amp;
                    maxInterface.IsAssignableFrom(typeInterface))
                {
                    maxInterface = typeInterface;
                    continueIterate = true;
                }
            }
        }

        return maxInterface;
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        if (value is null)
        {
            writer.WriteNullValue();
            return;
        }

        var finalType = IsForInterface ?
            FindMaxInterface(value.GetType()) :
            value.GetType();

        var node = JsonSerializer.SerializeToNode(value, finalType, options);

        if (node is JsonObject jObj)
        {
            jObj[&quot;$type&quot;] = finalType.FullName;
            jObj.WriteTo(writer, options);
        }
        else
        {
            writer.WriteNullValue();
        }
    }

    public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.Null)
        {
            return default;
        }

        var node = JsonNode.Parse(ref reader);

        if (node is not JsonObject payload)
        {
            throw new JsonException($&quot;Expected JSON object for interface type &apos;{typeToConvert}&apos;.&quot;);
        }

        if (SingletonReferenceHandler.TryResolveReference(payload, options, out var referenceValue))
        {
            return (T)referenceValue;
        }

        if (payload.TryGetPropertyValue(&quot;$type&quot;, out var typeJson) &amp;amp;&amp;amp;
            typeJson is not null &amp;amp;&amp;amp;
            typeJson.GetValueKind() == JsonValueKind.String)
        {
            typeToConvert = TypeLookupAssemblies
                .Select(asm =&amp;gt; asm.GetType(typeJson.GetValue&amp;lt;string&amp;gt;()))
                .Where(t =&amp;gt; t is not null)
                .First()!;

            payload.Remove(&quot;$type&quot;);
        }

        var value = JsonSerializer.Deserialize(payload, typeToConvert, options)!;

        return (T?)value;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是, 当你有这样的类型时:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A { public string? MemberA { get; set; } public string SomePropertyOnlyInClass =&amp;gt; &quot;balabala&quot;; }
class B : A, IB { public string? MemberB { get; set; } }
class C : A, IC { public string? MemberC { get; set; } }

interface IA { public string? MemberA { get; } }
interface IB : IA { public string? MemberB { get; } }
interface IC : IA { public string? MemberC { get; } }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就可以这样序列化:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A a = new C() { MemberA = &quot;abc&quot;, MemberC = &quot;def&quot; };
var json = JsonSerializer.Serialize(a, new JsonSerializerOptions()
{
    Converters = 
    {
        new AutoPolymorphicConverter&amp;lt;A&amp;gt;()
    }
});

Console.WriteLine(json);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码输出:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;MemberC&quot;:&quot;def&quot;,&quot;MemberA&quot;:&quot;abc&quot;,&quot;SomePropertyOnlyInClass&quot;:&quot;balabala&quot;,&quot;$type&quot;:&quot;C&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你想让它自动识别接口, 并仅序列化接口成员, 也可以这么做:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IA a2 = new C() { MemberA = &quot;abc&quot;, MemberC = &quot;def&quot; };
var json2 = JsonSerializer.Serialize(a2, new JsonSerializerOptions()
{
    Converters =
    {
        new AutoPolymorphicConverter&amp;lt;IA&amp;gt;() { IsForInterface = true }
    }
});

Console.WriteLine(json2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码输出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;MemberC&quot;:&quot;def&quot;,&quot;MemberA&quot;:&quot;abc&quot;,&quot;$type&quot;:&quot;IC&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个转换器处理的是声明类型和运行时类型不一致的问题.&lt;/p&gt;
&lt;p&gt;正常情况下, 如果你用基类或接口变量保存派生类对象:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IA value = new C() { MemberA = &quot;abc&quot;, MemberC = &quot;def&quot; };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;JsonSerializer.Serialize(value)&lt;/code&gt; 只知道声明类型是 &lt;code&gt;IA&lt;/code&gt;, 它没有理由自动把 &lt;code&gt;C&lt;/code&gt; 或 &lt;code&gt;IC&lt;/code&gt; 上的成员也写进去. 所以我们在序列化时主动做两件事:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找到真正要序列化的类型, 对类来说是运行时类型, 对接口来说是最具体的接口&lt;/li&gt;
&lt;li&gt;把这个类型的完整名称写入 &lt;code&gt;$type&lt;/code&gt;, 反序列化时再用这个名称找回类型&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果是接口多态, &lt;code&gt;IsForInterface = true&lt;/code&gt; 会让转换器调用 &lt;code&gt;FindMaxInterface&lt;/code&gt;, 从对象实现的接口中找到最具体的那个接口. 这样输出的是接口契约里的成员, 而不是实现类里所有公开属性. 这在跨进程插件场景很有用, 因为另一边通常只关心接口, 不应该依赖实现类细节.&lt;/p&gt;
&lt;p&gt;实际项目中, 推荐这样配置:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var polymorphicConverter = new AutoPolymorphicConverter&amp;lt;IA&amp;gt;()
{
    IsForInterface = true
};
polymorphicConverter.TypeLookupAssemblies.Add(typeof(IA).Assembly);

var options = new JsonSerializerOptions()
{
    ReferenceHandler = new SingletonReferenceHandler(),
    WriteIndented = true,
    Converters =
    {
        polymorphicConverter
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果序列化结果会跨进程或跨版本传输, 不建议直接暴露任意程序集中的任意类型. &lt;code&gt;TypeLookupAssemblies&lt;/code&gt; 本质上就是一个白名单, 反序列化时只从这些程序集里查类型, 这样至少不会随便从所有已加载程序集里解析类型.&lt;/p&gt;
&lt;h2&gt;自动调用目标类型构造函数&lt;/h2&gt;
&lt;p&gt;如果你某个类型没有公开的构造函数, 还希望反序列化它, 可以使用 System.Text.Json 中自带的 &lt;code&gt;JsonConstructor&lt;/code&gt; 特性.&lt;/p&gt;
&lt;p&gt;但很遗憾, 这个功能貌似与 &lt;code&gt;ReferenceHandler&lt;/code&gt; 不兼容. 所以我们需要自己写一个转换器, 用来自动调用带参数的构造函数.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class CustomConstructorConverter : JsonConverter&amp;lt;object&amp;gt;
{
    private static ConstructorInfo? FindConstructor(Type type)
    {
        var constructors = type.GetConstructors();

        foreach (var constructor in constructors)
        {
            if (constructor.IsPublic &amp;amp;&amp;amp;
                constructor.GetParameters().Length == 0)
            {
                return null;
            }
        }

        foreach (var constructor in constructors)
        {
            if (constructor.GetCustomAttribute&amp;lt;JsonConstructorAttribute&amp;gt;() is not null)
            {
                return constructor;
            }
        }

        return null;
    }

    private static ParameterInfo? FindParameter(ParameterInfo[] parameters, string parameterName)
    {
        foreach (var parameter in parameters)
        {
            if (string.Equals(parameter.Name, parameterName, StringComparison.OrdinalIgnoreCase))
            {
                return parameter;
            }
        }

        return null;
    }

    public override bool CanConvert(Type typeToConvert)
        =&amp;gt; FindConstructor(typeToConvert) != null;

    public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
        {
            throw new InvalidOperationException();
        }

        var jObj = JsonNode.Parse(ref reader) as JsonObject;
        if (jObj is null)
            return null;

        if (SingletonReferenceHandler.TryResolveReference(jObj, options, out var referenceValue))
        {
            return referenceValue;
        }

        var constructor = FindConstructor(typeToConvert)!;
        var constructorParameters = constructor.GetParameters();
        var parameterValues = new object?[constructorParameters.Length];
        foreach (var kv in jObj)
        {
            if (FindParameter(constructorParameters, kv.Key) is not { } parameter)
            {
                continue;
            }

            parameterValues[parameter.Position] = JsonSerializer.Deserialize(kv.Value, parameter.ParameterType, options);
        }

        var value = constructor.Invoke(parameterValues);
        SingletonReferenceHandler.AddReferenceIsNecessary(jObj, value, options);

        return value;
    }

    public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
    {
        throw new NotSupportedException();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此转换器只需要加入到解析 options 中的转换器, 就可以为所有没有无参构造的类型, 找到第一个公开构造函数, 并解析 JSON, 然后尝试构造.&lt;/p&gt;
&lt;p&gt;值得一提的是, 构造函数的参数名称必须和属性名称一致, 大小写不敏感. 否则转换器无法为构造函数准备参数值.&lt;/p&gt;
&lt;p&gt;例如:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserInfo
{
    [JsonConstructor]
    public UserInfo(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public string Name { get; }
    public int Age { get; }
}

var options = new JsonSerializerOptions()
{
    ReferenceHandler = new SingletonReferenceHandler(),
    Converters =
    {
        new CustomConstructorConverter()
    }
};

var user = JsonSerializer.Deserialize&amp;lt;UserInfo&amp;gt;(
    &quot;&quot;&quot;{ &quot;Name&quot;: &quot;slime&quot;, &quot;Age&quot;: 18 }&quot;&quot;&quot;,
    options)!;

Console.WriteLine(user.Name);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它的原理是先把整个对象读成 &lt;code&gt;JsonObject&lt;/code&gt;, 再根据构造函数参数名到 JSON 属性里找值. 找到之后, 对每个参数类型继续调用 &lt;code&gt;JsonSerializer.Deserialize&lt;/code&gt;, 最后通过 &lt;code&gt;ConstructorInfo.Invoke&lt;/code&gt; 创建对象.&lt;/p&gt;
&lt;p&gt;为什么要先读成 &lt;code&gt;JsonObject&lt;/code&gt;, 而不是直接一边读一边构造? 因为构造函数参数顺序不一定和 JSON 属性顺序一致. 先读成对象树之后, 就可以按属性名匹配参数, 不依赖 JSON 的字段顺序.&lt;/p&gt;
&lt;p&gt;这个转换器只处理构造函数参数, 不会在构造之后继续给其它可写属性赋值. 如果你的类型既有构造函数参数, 又有额外可写属性, 需要继续补充一段属性赋值逻辑, 或者保持类型设计简单一点: 需要反序列化的成员都进入构造函数.&lt;/p&gt;
&lt;h2&gt;只读集合类型的数据成员填充&lt;/h2&gt;
&lt;p&gt;如果正常情况下你有一个集合是只读的, 并且你还希望反序列化的时候把成员填充进已有的集合中, 那么使用 &lt;code&gt;JsonObjectCreationHandling.Populate&lt;/code&gt; 就可以做到.
但是很遗憾, 这个功能同样跟 &lt;code&gt;ReferenceHandler&lt;/code&gt; 存在兼容问题, 所以又到了我们最喜欢的手搓 JsonConverter 时刻!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AutoPopulateMemberCollectionConverter&amp;lt;T&amp;gt; : JsonConverter&amp;lt;T&amp;gt;
{
    private static void PopulateChildrenIntoCollection(
        object? targetObject,
        JsonArray children,
        JsonSerializerOptions options)
    {
        if (targetObject is null)
            throw new InvalidOperationException(&quot;The collection value to populate is null.&quot;);

        var targetType = targetObject.GetType();

        // Array / T[]
        if (targetObject is Array array)
        {
            var itemType = targetType.GetElementType()
                ?? throw new InvalidOperationException(&quot;Can not determine array element type.&quot;);

            if (array.Length &amp;lt; children.Count)
                throw new InvalidOperationException(&quot;Array length is insufficient to populate all child elements.&quot;);

            for (var i = 0; i &amp;lt; children.Count; i++)
            {
                var value = JsonSerializer.Deserialize(children[i], itemType, options);
                array.SetValue(value, i);
            }

            return;
        }

        // IList&amp;lt;T&amp;gt;
        var collectionInterface = targetType
            .GetInterfaces()
            .Concat(new[] { targetType })
            .FirstOrDefault(t =&amp;gt;
                t.IsGenericType &amp;amp;&amp;amp;
                t.GetGenericTypeDefinition() == typeof(ICollection&amp;lt;&amp;gt;));

        if (collectionInterface is not null)
        {
            var itemType = collectionInterface.GetGenericArguments()[0];

            var isReadOnlyProperty = collectionInterface.GetProperty(&quot;IsReadOnly&quot;);
            if (isReadOnlyProperty?.GetValue(targetObject) is true)
                throw new InvalidOperationException(&quot;The collection to populate is not writable.&quot;);

            var addMethod = collectionInterface.GetMethod(&quot;Add&quot;)
                ?? throw new InvalidOperationException(&quot;The collection to populate is not writable.&quot;);

            foreach (var item in children)
            {
                var value = JsonSerializer.Deserialize(item, itemType, options);
                addMethod.Invoke(targetObject, new[] { value });
            }

            return;
        }

        // non-generic IList
        if (targetObject is IList list)
        {
            if (list.IsReadOnly || list.IsFixedSize)
                throw new InvalidOperationException(&quot;The collection to populate is not writable.&quot;);

            foreach (var item in children)
            {
                var value = JsonSerializer.Deserialize&amp;lt;object&amp;gt;(item, options);
                list.Add(value);
            }

            return;
        }

        throw new InvalidOperationException(&quot;The object to populate is not a writable collection.&quot;);
    }

    public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.Null)
        {
            return default;
        }

        var node = JsonNode.Parse(ref reader);

        if (node is not JsonObject payload)
        {
            throw new JsonException($&quot;Expected JSON object for interface type &apos;{typeToConvert}&apos;.&quot;);
        }

        if (SingletonReferenceHandler.TryResolveReference(payload, options, out var referenceValue))
        {
            return (T?)referenceValue;
        }

        var subOptions = new JsonSerializerOptions(options);
        subOptions.Converters.Remove(this);

        var value = (T?)JsonSerializer.Deserialize(payload, typeToConvert, subOptions);

        if (value is { })
        {
            var actualType = value.GetType();
            foreach (var property in actualType.GetProperties())
            {
                if (property.CanWrite ||
                    !payload.TryGetPropertyValue(property.Name, out var propertyValueNode) ||
                    propertyValueNode is null)
                {
                    continue;
                }

                if (propertyValueNode.GetValueKind() == JsonValueKind.Array ||
                    (propertyValueNode.GetValueKind() == JsonValueKind.Object &amp;amp;&amp;amp;
                    propertyValueNode.AsObject().TryGetPropertyValue(&quot;$values&quot;, out propertyValueNode) &amp;amp;&amp;amp;
                    propertyValueNode is JsonArray))
                {
                    var collection = property.GetValue(value);
                    PopulateChildrenIntoCollection(collection, (JsonArray)propertyValueNode, options);
                }
            }
        }

        return value;
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        options.Converters.Remove(this);
        JsonSerializer.Serialize(writer, value, options);
        options.Converters.Add(this);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用的时候, 假如你有一个类型 &lt;code&gt;A&lt;/code&gt;, 它有一个只读的集合属性, 例如 &lt;code&gt;public List&amp;lt;int&amp;gt; SomeCollection { get; } = new();&lt;/code&gt;,
那么在反序列化的时候, 在 &lt;code&gt;Converters&lt;/code&gt; 列表最前方添加一个 &lt;code&gt;AutoPopulateMemberCollectionConverter&amp;lt;A&amp;gt;&lt;/code&gt; 就可以了.&lt;/p&gt;
&lt;p&gt;如果放到后面, 类型实例化可能会优先被其他转换器执行, 导致无法自动填充成员. 此转换器会自动使用后续转换器来转换, 得出值之后, 再进行填充.&lt;/p&gt;
&lt;p&gt;使用示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Menu
{
    public string Name { get; set; } = &quot;&quot;;
    public List&amp;lt;MenuItem&amp;gt; Items { get; } = new();
}

public class MenuItem
{
    public string Text { get; set; } = &quot;&quot;;
}

var options = new JsonSerializerOptions()
{
    ReferenceHandler = new SingletonReferenceHandler(),
    Converters =
    {
        new AutoPopulateMemberCollectionConverter&amp;lt;Menu&amp;gt;()
    }
};

var menu = JsonSerializer.Deserialize&amp;lt;Menu&amp;gt;(
    &quot;&quot;&quot;
    {
      &quot;Name&quot;: &quot;File&quot;,
      &quot;Items&quot;: [
        { &quot;Text&quot;: &quot;Open&quot; },
        { &quot;Text&quot;: &quot;Save&quot; }
      ]
    }
    &quot;&quot;&quot;,
    options)!;

Console.WriteLine(menu.Items.Count); // 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个转换器分成两步:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先临时移除自己, 让 &lt;code&gt;System.Text.Json&lt;/code&gt; 用默认逻辑把对象主体创建出来&lt;/li&gt;
&lt;li&gt;再检查对象的只读集合属性, 如果 JSON 中有对应数组, 就把数组中的元素逐个反序列化并添加到已有集合里&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;也就是说, 它不是替代整个对象的反序列化流程, 而是在默认流程之后补一刀. 这样它可以少管很多事情, 例如普通属性赋值, 构造函数选择, 数字字符串转换等都交给原本的序列化器处理.&lt;/p&gt;
&lt;h2&gt;将所有转换器组合起来&lt;/h2&gt;
&lt;p&gt;上面这些转换器并不是都能同时用于序列化和反序列化.&lt;/p&gt;
&lt;p&gt;例如:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;InterfaceProxyConverter&lt;/code&gt; 只支持读, 它的作用是反序列化接口代理, &lt;code&gt;Write&lt;/code&gt; 直接抛出 &lt;code&gt;NotSupportedException&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CustomConstructorConverter&lt;/code&gt; 只支持读, 它的作用是调用目标类型的构造函数, &lt;code&gt;Write&lt;/code&gt; 也直接抛出 &lt;code&gt;NotSupportedException&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AutoPopulateMemberCollectionConverter&amp;lt;T&amp;gt;&lt;/code&gt; 主要解决读的时候填充只读集合&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CommonParseConverter&lt;/code&gt; 读写都支持&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AutoPolymorphicConverter&amp;lt;T&amp;gt;&lt;/code&gt; 读写都支持, 但序列化时主要负责写入 &lt;code&gt;$type&lt;/code&gt;, 反序列化时还需要通过 &lt;code&gt;TypeLookupAssemblies&lt;/code&gt; 找回类型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以实际使用时, 不应该强行把所有转换器都丢到同一个 &lt;code&gt;JsonSerializerOptions&lt;/code&gt; 里. 更合理的方式是准备两套 Options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一套用于序列化, 只放写入阶段需要的转换器&lt;/li&gt;
&lt;li&gt;一套用于反序列化, 只放读取阶段需要的转换器&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;假设有这样的模型:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class A : IA
{
    public string? MemberA { get; set; }
    public IA? Other { get; set; }
    public string? SomePropertyOnlyInClass { get; set; }
    public List&amp;lt;int&amp;gt; SomeCollection { get; } = new();
    public CustomContructorClass? CCCValue { get; set; }
    public Int2 Vector { get; set; }
}

public class B : A, IB
{
    public string? MemberB { get; set; }
}

public class C : A, IC
{
    public string? MemberC { get; set; }
}

public class CustomContructorClass
{
    public CustomContructorClass(string a, string b)
    {
        A = a;
        B = b;
    }

    public string A { get; }
    public string B { get; }
}

public record struct Int2(int X, int Y)
{
    public static Int2 Parse(string s)
    {
        var parts = s.Split(&apos;,&apos;);
        return new Int2(int.Parse(parts[0]), int.Parse(parts[1]));
    }

    public override string ToString()
        =&amp;gt; $&quot;{X},{Y}&quot;;
}

public interface IA
{
    string? MemberA { get; set; }
    IA? Other { get; set; }
}

public interface IB : IA
{
    string? MemberB { get; set; }
}

public interface IC : IA
{
    string? MemberC { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再创建一份带循环引用的对象:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A complexObject = new C()
{
    MemberA = &quot;ValueA&quot;,
    SomePropertyOnlyInClass = &quot;QWQ&quot;,
    SomeCollection =
    {
        1, 2, 3, 4, 5
    },
    CCCValue = new CustomContructorClass(&quot;A&quot;, &quot;B&quot;),
    Vector = new Int2(10, 20)
};

B complexObject2 = new B()
{
    MemberA = &quot;ValueA&quot;,
    MemberB = &quot;ValueB&quot;,
    SomePropertyOnlyInClass = &quot;AWA&quot;,
    SomeCollection =
    {
        7, 8, 9, 10
    }
};

complexObject.Other = complexObject2;
complexObject2.Other = complexObject;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后准备同一个 &lt;code&gt;SingletonReferenceHandler&lt;/code&gt;, 以及两套 Options:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var singletonReferenceHandler = new SingletonReferenceHandler();

var serializeOptions = new JsonSerializerOptions()
{
    WriteIndented = true,
    ReferenceHandler = singletonReferenceHandler,
    Converters =
    {
        new CommonParseConverter(),
        new AutoPolymorphicConverter&amp;lt;A&amp;gt;(),
        new AutoPolymorphicConverter&amp;lt;IA&amp;gt;() { IsForInterface = true },
    }
};

var deserializeOptions = new JsonSerializerOptions()
{
    ReferenceHandler = singletonReferenceHandler,
    Converters =
    {
        new AutoPopulateMemberCollectionConverter&amp;lt;A&amp;gt;(),
        new CommonParseConverter(),
        new AutoPolymorphicConverter&amp;lt;A&amp;gt;()
        {
            TypeLookupAssemblies = { typeof(A).Assembly }
        },
        new AutoPolymorphicConverter&amp;lt;IA&amp;gt;()
        {
            IsForInterface = true,
            TypeLookupAssemblies = { typeof(A).Assembly }
        },
        new InterfaceProxyConverter()
        {
            TypesToProxy = { typeof(IA) },
            TypeLookupAssemblies = { typeof(A).Assembly }
        },
        new CustomConstructorConverter(),
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里最关键的是: 序列化和反序列化都使用同一个 &lt;code&gt;SingletonReferenceHandler&lt;/code&gt;, 但每次开始一次新的操作前都要 &lt;code&gt;Reset&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;singletonReferenceHandler.Reset();
var json = JsonSerializer.Serialize(complexObject, serializeOptions);

singletonReferenceHandler.Reset();
var deserializedObject = JsonSerializer.Deserialize&amp;lt;A&amp;gt;(json, deserializeOptions);

singletonReferenceHandler.Reset();
var json2 = JsonSerializer.Serialize(deserializedObject, serializeOptions);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一次序列化可能得到这样的 JSON:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;$id&quot;: &quot;1&quot;,
  &quot;MemberC&quot;: null,
  &quot;MemberA&quot;: &quot;ValueA&quot;,
  &quot;Other&quot;: {
    &quot;$id&quot;: &quot;2&quot;,
    &quot;MemberB&quot;: &quot;ValueB&quot;,
    &quot;MemberA&quot;: &quot;ValueA&quot;,
    &quot;Other&quot;: {
      &quot;$ref&quot;: &quot;1&quot;,
      &quot;$type&quot;: &quot;AdvancedJsonSerialization.IC&quot;
    },
    &quot;$type&quot;: &quot;AdvancedJsonSerialization.IB&quot;
  },
  &quot;SomePropertyOnlyInClass&quot;: &quot;QWQ&quot;,
  &quot;SomeCollection&quot;: {
    &quot;$id&quot;: &quot;3&quot;,
    &quot;$values&quot;: [
      1,
      2,
      3,
      4,
      5
    ]
  },
  &quot;CCCValue&quot;: {
    &quot;$id&quot;: &quot;4&quot;,
    &quot;A&quot;: &quot;A&quot;,
    &quot;B&quot;: &quot;B&quot;
  },
  &quot;Vector&quot;: &quot;10,20&quot;,
  &quot;$type&quot;: &quot;AdvancedJsonSerialization.C&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 JSON 中同时包含了几个关键信息:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;$id&lt;/code&gt; 和 &lt;code&gt;$ref&lt;/code&gt; 保存了对象引用关系&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$type&lt;/code&gt; 保存了运行时类型或更具体的接口类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SomeCollection&lt;/code&gt; 使用 &lt;code&gt;$values&lt;/code&gt; 保存集合内容&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CCCValue&lt;/code&gt; 虽然没有无参构造函数, 但 JSON 中保留了构造函数所需的属性&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Vector&lt;/code&gt; 被写成字符串, 反序列化时通过 &lt;code&gt;Int2.Parse&lt;/code&gt; 读回&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果反序列化之后再使用同一套 &lt;code&gt;serializeOptions&lt;/code&gt; 序列化一次, 得到的 JSON 应当和前面基本一致. 这就说明循环引用, 多态类型, 接口代理, 构造函数对象, 只读集合和 &lt;code&gt;Parse&lt;/code&gt; 类型都被正确还原了.&lt;/p&gt;
&lt;h2&gt;两套 Options 的转换器顺序&lt;/h2&gt;
&lt;p&gt;序列化 Options 的顺序比较简单:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Converters =
{
    new CommonParseConverter(),
    new AutoPolymorphicConverter&amp;lt;A&amp;gt;(),
    new AutoPolymorphicConverter&amp;lt;IA&amp;gt;() { IsForInterface = true },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;CommonParseConverter&lt;/code&gt; 负责把 &lt;code&gt;Int2&lt;/code&gt; 这类类型写成字符串. &lt;code&gt;AutoPolymorphicConverter&amp;lt;A&amp;gt;&lt;/code&gt; 负责类多态, 也就是把实际的 &lt;code&gt;C&lt;/code&gt; 写出来. &lt;code&gt;AutoPolymorphicConverter&amp;lt;IA&amp;gt;&lt;/code&gt; 负责接口多态, 也就是当成员声明为 &lt;code&gt;IA&lt;/code&gt; 但实际对象实现了 &lt;code&gt;IB&lt;/code&gt; 或 &lt;code&gt;IC&lt;/code&gt; 时, 写入更具体的接口类型.&lt;/p&gt;
&lt;p&gt;反序列化 Options 的顺序更重要:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Converters =
{
    new AutoPopulateMemberCollectionConverter&amp;lt;A&amp;gt;(),
    new CommonParseConverter(),
    new AutoPolymorphicConverter&amp;lt;A&amp;gt;()
    {
        TypeLookupAssemblies = { typeof(A).Assembly }
    },
    new AutoPolymorphicConverter&amp;lt;IA&amp;gt;()
    {
        IsForInterface = true,
        TypeLookupAssemblies = { typeof(A).Assembly }
    },
    new InterfaceProxyConverter()
    {
        TypesToProxy = { typeof(IA) },
        TypeLookupAssemblies = { typeof(A).Assembly }
    },
    new CustomConstructorConverter(),
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;AutoPopulateMemberCollectionConverter&amp;lt;A&amp;gt;&lt;/code&gt; 要放在前面, 因为它需要包住 &lt;code&gt;A&lt;/code&gt; 的默认反序列化流程, 等对象创建完成后再填充只读集合.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CommonParseConverter&lt;/code&gt; 需要在遇到 &lt;code&gt;Int2&lt;/code&gt; 这种字符串值类型时接管读取.&lt;/p&gt;
&lt;p&gt;两个 &lt;code&gt;AutoPolymorphicConverter&lt;/code&gt; 分别处理类和接口上的 &lt;code&gt;$type&lt;/code&gt;. 读侧一定要设置 &lt;code&gt;TypeLookupAssemblies&lt;/code&gt;, 否则它只读到了类型名称, 但不知道应该去哪一个程序集里找这个类型.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;InterfaceProxyConverter&lt;/code&gt; 放在多态转换器后面, 用来真正创建接口代理. 它同样需要 &lt;code&gt;TypesToProxy&lt;/code&gt; 和 &lt;code&gt;TypeLookupAssemblies&lt;/code&gt;: 前者限制哪些接口可以代理, 后者用于根据 &lt;code&gt;$type&lt;/code&gt; 找到更具体的接口类型.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CustomConstructorConverter&lt;/code&gt; 放在后面兜底处理没有无参构造函数的类型. 在上面的例子里, &lt;code&gt;CustomContructorClass&lt;/code&gt; 就是通过它还原的.&lt;/p&gt;
&lt;h2&gt;局限和注意事项&lt;/h2&gt;
&lt;p&gt;这套方案的重点是保存和恢复复杂对象图, 不是替代所有 DTO.&lt;/p&gt;
&lt;p&gt;首先, 要分清楚哪些转换器能写, 哪些转换器能读. 如果把 &lt;code&gt;InterfaceProxyConverter&lt;/code&gt; 或 &lt;code&gt;CustomConstructorConverter&lt;/code&gt; 放进序列化 Options, 并且刚好命中它们的 &lt;code&gt;CanConvert&lt;/code&gt;, 就会因为 &lt;code&gt;Write&lt;/code&gt; 抛出 &lt;code&gt;NotSupportedException&lt;/code&gt; 而失败.&lt;/p&gt;
&lt;p&gt;其次, &lt;code&gt;TypeLookupAssemblies&lt;/code&gt; 不只是为了让代码能跑, 它也相当于类型解析范围的限制. 不要对外部不可信 JSON 开放任意类型解析. 如果 JSON 来自不可信来源, 最好在程序集限制之外再做一层类型白名单.&lt;/p&gt;
&lt;p&gt;另外, &lt;code&gt;SingletonReferenceHandler&lt;/code&gt; 不是全局缓存. 它只是为了让一次序列化或反序列化流程中的多次 &lt;code&gt;JsonSerializer&lt;/code&gt; 调用共享同一个引用解析器. 每次开始新的序列化或反序列化前都应该调用 &lt;code&gt;Reset&lt;/code&gt;, 或者直接为每次操作创建新的 Handler 和 Options.&lt;/p&gt;
&lt;p&gt;最后, 动态代理只适合这种“属性接口”模型. 如果接口里有普通方法, 当前 &lt;code&gt;InterfaceInterceptor&lt;/code&gt; 并不会处理方法调用. 真要跨进程调用方法, 应该额外设计 RPC 或消息协议, 不应该指望 JSON 反序列化自动解决方法执行.&lt;/p&gt;
</content:encoded></item><item><title>[.NET] 基于 Silk.NET DirectX 几何着色器的大量四边形渲染</title><link>https://slimenull.com/posts/20250623010000/</link><guid isPermaLink="true">https://slimenull.com/posts/20250623010000/</guid><description>基于 Silk.NET DirectX 11 几何着色器的四边形渲染方案，一次绘制调用渲染大量四边形</description><pubDate>Mon, 23 Jun 2025 01:00:00 GMT</pubDate><content:encoded>&lt;p&gt;假如我们需要渲染数以万计的四边形, 包括描边颜色和填充颜色, 并且要求高帧率, 那么单纯使用诸如 GDI+, Skia 这类 CPU 绘图库, 速度无疑是不够的.&lt;/p&gt;
&lt;p&gt;在最初, 我想着自己生成一堆顶点, 三角形索引, 让 DirectX 做渲染, 后来我发现, 如果是根据多边形顶点, 构建包含描边色带和填充区域的网格, 就已经非常耗时间了. 况且从内存拷贝到 GPU 的传输损耗也不容小视.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;其实后来发现, 我应该重用 VertexBuffer, 并在使用的时候, Map, 然后写入&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;直到, 我知道了有个东西, 叫做几何着色器. 我完全可以把 &quot;构建网格&quot; 的过程, 交给 GPU 的几何着色器来处理.&lt;/p&gt;
&lt;h2&gt;几何着色器&lt;/h2&gt;
&lt;p&gt;几何着色器（Geometry Shader）是 DirectX 10 引入的可编程管线阶段，它运行在顶点着色器之后、光栅化之前。它的强大之处在于&lt;strong&gt;能够从输入的图元生成新的图元&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对于我们的四边形渲染，流程是这样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;输入&lt;/strong&gt;：单个点（包含位置、大小、旋转、颜色等信息）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;几何着色器处理&lt;/strong&gt;：根据这个点的信息生成多个三角形&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;输出&lt;/strong&gt;：形成四边形的三角形流&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样做的好处显而易见：&lt;strong&gt;数据传输量大幅减少，GPU 利用率提升&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;几何着色器的基本结构&lt;/h2&gt;
&lt;p&gt;几何着色器的基本语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[maxvertexcount(15)]
void GS(point GS_INPUT input[1], inout TriangleStream&amp;lt;PS_INPUT&amp;gt; triStream)
{
    // 几何着色器实现
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键要素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;maxvertexcount(15)&lt;/code&gt;：指定这个几何着色器最多输出多少个顶点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;point GS_INPUT input[1]&lt;/code&gt;：输入是一个点数组（在我们的例子中只有一个点）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inout TriangleStream&amp;lt;PS_INPUT&amp;gt; triStream&lt;/code&gt;：输出三角形流&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;三角形条带&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;TriangleStream&lt;/code&gt; 表示要输出的三角形流, 调用其 &lt;code&gt;Append&lt;/code&gt; 方法可向其中添加一个顶点. 但添加时并不是每三个顶点构成一个三角形, 而是各个顶点之间构成一个&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/direct3d9/triangle-strips&quot;&gt;三角形条带&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;假设有一个正四边形, 其顺时针方向的四个顶点分别是 v0, v1, v2, v3. 正常的网格可能需要这样的数据, 顶点列表 [v0, v1, v2, v3], 三角形索引列表 [0, 1, 2, 0, 2, 3].&lt;/p&gt;
&lt;p&gt;但是如果是三角形带, 只需要按照 [v0, v1, v2, v3, v0] 的顺序依次添加进去, 围成一个四边形即可.&lt;/p&gt;
&lt;p&gt;每次输出完一个三角形，需要调用 &lt;code&gt;RestartStrip()&lt;/code&gt; 来结束当前的三角形条带：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 输出描边三角形
triStream.Append(v[0]);
// ... 其他顶点
triStream.Append(v[10]);
triStream.RestartStrip();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;全局数据&lt;/h2&gt;
&lt;p&gt;为了定义一些全局数据, 并在着色器中使用, 我们需要定义一个 Constant Buffer。在我们的渲染器中，它包含：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cbuffer ScreenBuffer : register(b0)
{
    float2 screenSize;  // 屏幕尺寸
    float3x3 transform; // 变换矩阵
    float3 strokeWidthFactorAndSizeFactor; // 各种缩放因子
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里需要注意的是, Constant Buffer 是存在内存对齐的. 所以在 C# 中对应的数据, 是这样的:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ReadOnlySpan&amp;lt;float&amp;gt; constBufferData =
[
    _width,                 _height,            0,             0,    // 16字节对齐
    _transform.M11,         _transform.M12,     0,             0,    // 16字节对齐
    _transform.M21,         _transform.M22,     0,             0,    // 16字节对齐
    _transform.OffsetX,     _transform.OffsetY, 1,             0,    // 16字节对齐
    _strokeThicknessFactor, _widthFactor,       _heightFactor, 0     // 16字节对齐
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每行必须是 16 字节（4 个 float）的倍数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;float2&lt;/code&gt; 会被填充到 16 字节&lt;/li&gt;
&lt;li&gt;&lt;code&gt;float3&lt;/code&gt; 也会被填充到 16 字节&lt;/li&gt;
&lt;li&gt;不对齐会导致 GPU 读取错误的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;几何着色器中的四边形生成&lt;/h2&gt;
&lt;p&gt;我们的几何着色器需要从一个点生成带描边的四边形：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[maxvertexcount(30)]
void GS(point GS_INPUT input[1], inout TriangleStream&amp;lt;PS_INPUT&amp;gt; triStream)
{
    GS_INPUT i = input[0];
  
    // 计算实际尺寸
    float strokeWidth = i.StrokeWidth * strokeWidthFactorAndSizeFactor.x;
    float2 size = float2(
        i.Size.x * strokeWidthFactorAndSizeFactor.y, 
        i.Size.y * strokeWidthFactorAndSizeFactor.z
    );
  
    // 计算内外矩形
    float2 halfSize = size * 0.5f;
    float2 outerHalfSize = halfSize + strokeWidth / 2;
    float2 innerHalfSize = halfSize - strokeWidth / 2;
  
    // 生成旋转矩阵
    float sinR = sin(i.Rotation);
    float cosR = cos(i.Rotation);
    float2x2 rotMatrix = float2x2(cosR, sinR, -sinR, cosR);
  
    // 计算 8 个关键顶点（外矩形4个 + 内矩形4个）
    // ... 顶点计算代码
  
    // 输出描边三角形
    // 输出填充三角形
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;坐标系统的转换&lt;/h2&gt;
&lt;p&gt;从屏幕坐标到裁剪空间的转换是关键环节：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float2 ScreenToClipPoint(float2 input)
{
    // 应用变换矩阵
    float3 pos = float3(input, 1.0f);
    float3 transformedPos = mul(transform, pos);
  
    // 转换到裁剪空间 (-1 到 1)
    float2 normalizedPos;
    normalizedPos.x = (transformedPos.x / screenSize.x) * 2.0f - 1.0f;
    normalizedPos.y = 1.0f - (transformedPos.y / screenSize.y) * 2.0f; // Y轴翻转
  
    return normalizedPos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 Y 轴的翻转，因为 DirectX 的屏幕坐标原点在左上角，而裁剪空间的原点在中心。&lt;/p&gt;
&lt;h2&gt;仓库&lt;/h2&gt;
&lt;p&gt;完整的测试代码仓库如下:&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;SlimeNull/LibDxGeometryRendering&quot;}&lt;/p&gt;
</content:encoded></item><item><title>[WPF] 实现真正的背景模糊</title><link>https://slimenull.com/posts/20250608020400/</link><guid isPermaLink="true">https://slimenull.com/posts/20250608020400/</guid><description>我会是地球上第一个真正实现 WPF 背景模糊的程序员</description><pubDate>Sun, 08 Jun 2025 02:04:00 GMT</pubDate><content:encoded>&lt;p&gt;在 Web 中, 只需要一句 CSS 代码, 即可实现任何元素的背景模糊:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.some-class {
    backdrop-filter: blur(10px);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但很遗憾, WPF 并不能这样实现. 一切, 都要我们自己&lt;strong&gt;手搓&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;我们需要什么&lt;/h2&gt;
&lt;p&gt;要实现背景模糊, 我们首先需要得到背景, 再对其进行模糊. 假设我们有这样的一个层级关系:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;根
 |- 某些父节点
 |   |- 某些父节点之前的兄弟
 |   |- 某些父节点之前的兄弟
 |   |- 某些父节点
 |   |   |- 某些优先绘制的兄弟
 |   |   |- 某些优先绘制的兄弟
 |   |   |- 自身
 |   |   |- 后面的兄弟
 |   |   |- 后面的兄弟...
 |   |- ...
 |- ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么我们的 &quot;背景&quot; 来自于所有 &quot;在自己之前&quot; 的&lt;strong&gt;同级节点&lt;/strong&gt;, 自己的&lt;strong&gt;父节点&lt;/strong&gt;, 然后是自己父节点之前的同级节点, 父节点的父节点, &lt;strong&gt;如此递归. 直到取到根&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;要获取非父节点元素的内容是非常简单的, 因为&lt;strong&gt;它们和我们自身是没有关系的&lt;/strong&gt;. 我们只需要使用 &lt;code&gt;VisualBrush&lt;/code&gt;, 就可以获取到它们的内容.&lt;/p&gt;
&lt;p&gt;但棘手的是父节点, 因为&lt;strong&gt;父节点包含我们自身&lt;/strong&gt;, 以及我们自身后面的兄弟节点. &lt;strong&gt;我们需要干净的父节点自身的内容&lt;/strong&gt;, 这是问题的关键.&lt;/p&gt;
&lt;p&gt;所以接下来, 我们要做的是一个会绘制自身背景的元素.&lt;/p&gt;
&lt;h2&gt;WPF 的渲染流程&lt;/h2&gt;
&lt;p&gt;我们知道, 如果要自定义一个元素的渲染, 只需要重写 &lt;code&gt;OnRender&lt;/code&gt; 方法, 并使用传入的 DrawingContext 进行绘制即可.&lt;/p&gt;
&lt;p&gt;通过阅读 WPF 的源代码, 我们可以发现, &lt;code&gt;OnRender&lt;/code&gt; 的调用是在 &lt;a href=&quot;https://learn.microsoft.com/zh-cn/dotnet/api/system.windows.uielement&quot;&gt;UIElement&lt;/a&gt; 的 &lt;a href=&quot;https://learn.microsoft.com/zh-cn/dotnet/api/system.windows.uielement.arrange&quot;&gt;Arrange&lt;/a&gt; 方法中. 位于 &lt;a href=&quot;https://source.dot.net/#PresentationCore/System/Windows/UIElement.cs,928&quot;&gt;UIElement.cs 的 928 行&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;而这里的 &lt;code&gt;DrawingContext&lt;/code&gt; 参数来自于 &lt;code&gt;UIElement&lt;/code&gt; 的内部方法, &lt;a href=&quot;https://source.dot.net/PresentationCore/R/b3a742a988d4f6a5.html&quot;&gt;RenderOpen&lt;/a&gt; 中. 它其实是创建了一个内部类 &lt;code&gt;VisualDrawingContext&lt;/code&gt; 的实例.&lt;/p&gt;
&lt;p&gt;在绘制完成后, 还调用了 &lt;code&gt;DrawingContext&lt;/code&gt; 的 &lt;code&gt;Close&lt;/code&gt; 方法, 经过一系列调用, 最后到达了 &lt;code&gt;UIElement&lt;/code&gt; 的 &lt;a href=&quot;https://source.dot.net/PresentationCore/R/c4d29c5305e78949.html&quot;&gt;RenderClose&lt;/a&gt; 方法.&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;RenderClose&lt;/code&gt; 方法本质上是将 &lt;code&gt;DrawingContext&lt;/code&gt; 的调用指令, 存储到了 &lt;code&gt;UIElement&lt;/code&gt; 的 &lt;code&gt;_drawingContent&lt;/code&gt; 字段中了.&lt;/p&gt;
&lt;p&gt;至此, 我们可以知道, &lt;code&gt;UIElement&lt;/code&gt; 如何呈现, 关键在于 &lt;code&gt;_drawingContent&lt;/code&gt; 的值.&lt;/p&gt;
&lt;h2&gt;取得元素绘制内容&lt;/h2&gt;
&lt;p&gt;通过反射, 我们可以轻易取到 &lt;code&gt;UIElement&lt;/code&gt; 的 &lt;code&gt;_drawingContent&lt;/code&gt; 字段的值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static readonly FieldInfo _drawingContentOfUIElement = typeof(UIElement)
    .GetField(&quot;_drawingContent&quot;, BindingFlags.Instance | BindingFlags.NonPublic)!;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Visual someVisual;
object drawingContent = _drawingContentOfUIElement.GetValue(someVisual);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来, 我们需要将其转移到一个新的 &lt;code&gt;Visual&lt;/code&gt; 中, 然后绘制这个新的 &lt;code&gt;Visual&lt;/code&gt;. 这里使用 &lt;code&gt;DrawingVisual&lt;/code&gt;.
因为如果使用 &lt;code&gt;UIElement&lt;/code&gt;, 在绘制时会调用其 &lt;code&gt;OnRender&lt;/code&gt; 重新进行绘制, 这没意义. 而且据笔者实验, 这总是会产生一些闪烁问题.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static readonly FieldInfo _contentOfDrawingVisual = typeof(DrawingVisual)
    .GetField(&quot;_content&quot;, BindingFlags.Instance | BindingFlags.NonPublic)!;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;object drawingContentOfSomeVisual;
DrawingVisual drawingVisual = new DrawingVisual();
_contentOfDrawingVisual.SetValue(drawingVisual, drawingContentOfSomeVisual);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在, 我们就可以通过 &lt;code&gt;VisualBrush&lt;/code&gt; 将我们所取得的内容绘制出来了.&lt;/p&gt;
&lt;h2&gt;内容的刷新&lt;/h2&gt;
&lt;p&gt;因为我们的内容来自父节点, 以及在自身之前的同级节点, 所以当它们位置变动, 也就是布局更新时, 我们需要重新绘制.&lt;/p&gt;
&lt;p&gt;这很简单, 我们只需要订阅父节点作为 UIElement 的 LayoutUpdated 事件即可:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected override void OnVisualParentChanged(DependencyObject oldParentObject)
{
    if (oldParentObject is UIElement oldParent)
    {
        oldParent.LayoutUpdated -= ParentLayoutUpdated;
    }

    if (Parent is UIElement newParent)
    {
        newParent.LayoutUpdated += ParentLayoutUpdated;
    }
}

private void ParentLayoutUpdated(object? sender, EventArgs e)
{
    // 在这里引发重绘
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过, 这里需要注意. 不能使用 &lt;code&gt;InvalidateVisual()&lt;/code&gt; 引发重绘, 因为它同样会使布局失效. 这样就造成死递归了(严格来讲会导致布局在每一帧都进行, 造成卡顿).&lt;/p&gt;
&lt;h2&gt;对内容进行裁剪&lt;/h2&gt;
&lt;p&gt;因为我们的渲染包含了背景的所有元素, 所以其大小也是和背景一致的. 它会覆盖整个窗口.
所以, 我们需要重写 GetLayoutClip 来设定呈现的范围.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected override Geometry GetLayoutClip(Size layoutSlotSize)
{
    return new RectangleGeometry(new Rect(0, 0, ActualWidth, ActualHeight));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;结合在一起&lt;/h2&gt;
&lt;p&gt;所有的所有, 结合在一起, 我们得到了一个背景呈现元素, 再对其添加 BlurEffect 效果, 就是实时背景模糊:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/wpf_background_blur.gif&quot; alt=&quot;realtime background blur effect&quot; /&gt;&lt;/p&gt;
&lt;p&gt;背景呈现元素的完整代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class BackgroundPresenter : FrameworkElement
{
    private static readonly FieldInfo _drawingContentOfUIElement = typeof(UIElement)
        .GetField(&quot;_drawingContent&quot;, BindingFlags.Instance | BindingFlags.NonPublic)!;

    private static readonly FieldInfo _contentOfDrawingVisual = typeof(DrawingVisual)
        .GetField(&quot;_content&quot;, BindingFlags.Instance | BindingFlags.NonPublic)!;

    private static readonly FieldInfo _offsetOfVisual = typeof(Visual)
        .GetField(&quot;_offset&quot;, BindingFlags.Instance | BindingFlags.NonPublic)!;

    private static readonly Func&amp;lt;UIElement, DrawingContext&amp;gt; _renderOpenMethod = typeof(UIElement)
        .GetMethod(&quot;RenderOpen&quot;, BindingFlags.Instance | BindingFlags.NonPublic)!
        .CreateDelegate&amp;lt;Func&amp;lt;UIElement, DrawingContext&amp;gt;&amp;gt;();

    private static readonly Action&amp;lt;UIElement, DrawingContext&amp;gt; _onRenderMethod = typeof(UIElement)
        .GetMethod(&quot;OnRender&quot;, BindingFlags.Instance | BindingFlags.NonPublic)!
        .CreateDelegate&amp;lt;Action&amp;lt;UIElement, DrawingContext&amp;gt;&amp;gt;();

    private static readonly GetContentBoundsDelegate _methodGetContentBounds = typeof(VisualBrush)
        .GetMethod(&quot;GetContentBounds&quot;, BindingFlags.Instance | BindingFlags.NonPublic)!
        .CreateDelegate&amp;lt;GetContentBoundsDelegate&amp;gt;();

    private delegate void GetContentBoundsDelegate(VisualBrush visualBrush, out Rect bounds);
    private readonly Stack&amp;lt;UIElement&amp;gt; _parentStack = new();

    private static void ForceRender(UIElement target)
    {
        using DrawingContext drawingContext = _renderOpenMethod(target);

        _onRenderMethod.Invoke(target, drawingContext);
    }

    private static void DrawVisual(DrawingContext drawingContext, Visual visual, Point relatedXY, Size renderSize)
    {
        var visualBrush = new VisualBrush(visual);
        var visualOffset = (Vector)_offsetOfVisual.GetValue(visual)!;

        _methodGetContentBounds.Invoke(visualBrush, out var contentBounds);
        relatedXY -= visualOffset;

        drawingContext.DrawRectangle(
            visualBrush, null,
            new Rect(relatedXY.X + contentBounds.X, relatedXY.Y + contentBounds.Y, contentBounds.Width, contentBounds.Height));
    }

    protected override Geometry GetLayoutClip(Size layoutSlotSize)
    {
        return new RectangleGeometry(new Rect(0, 0, ActualWidth, ActualHeight));
    }

    protected override void OnVisualParentChanged(DependencyObject oldParentObject)
    {
        if (oldParentObject is UIElement oldParent)
        {
            oldParent.LayoutUpdated -= ParentLayoutUpdated;
        }

        if (Parent is UIElement newParent)
        {
            newParent.LayoutUpdated += ParentLayoutUpdated;
        }
    }

    private void ParentLayoutUpdated(object? sender, EventArgs e)
    {
        // cannot use &apos;InvalidateVisual&apos; here, because it will cause infinite loop

        ForceRender(this);

        Debug.WriteLine(&quot;Parent layout updated, forcing render of BackgroundPresenter.&quot;);
    }

    private static void DrawBackground(
        DrawingContext drawingContext, UIElement self,
        Stack&amp;lt;UIElement&amp;gt; parentStackStorage,
        int maxDepth,
        bool throwExceptionIfParentArranging)
    {
#if DEBUG
        bool selfInDesignMode = DesignerProperties.GetIsInDesignMode(self);
#endif

        var parent = VisualTreeHelper.GetParent(self) as UIElement;
        while (
            parent is { } &amp;amp;&amp;amp;
            parentStackStorage.Count &amp;lt; maxDepth)
        {
            // parent not visible, no need to render
            if (!parent.IsVisible)
            {
                parentStackStorage.Clear();
                return;
            }

#if DEBUG
            if (selfInDesignMode &amp;amp;&amp;amp;
                parent.GetType().ToString().Contains(&quot;VisualStudio&quot;))
            {
                // 遍历到 VS 自身的设计器元素, 中断!
                break;
            }
#endif

            // is parent arranging
            // we cannot render it
            if (parent.RenderSize.Width == 0 ||
                parent.RenderSize.Height == 0)
            {
                parentStackStorage.Clear();

                if (throwExceptionIfParentArranging)
                {
                    throw new InvalidOperationException(&quot;Arranging&quot;);
                }

                // render after parent arranging finished
                self.InvalidateArrange();
                return;
            }

            parentStackStorage.Push(parent);
            parent = VisualTreeHelper.GetParent(parent) as UIElement;
        }

        var selfRect = new Rect(0, 0, self.RenderSize.Width, self.RenderSize.Height);
        while (parentStackStorage.TryPop(out var currentParent))
        {
            if (!parentStackStorage.TryPeek(out var breakElement))
            {
                breakElement = self;
            }

            var parentRelatedXY = currentParent.TranslatePoint(default, self);

            // has render data
            if (_drawingContentOfUIElement.GetValue(currentParent) is { } parentDrawingContent)
            {
                var drawingVisual = new DrawingVisual();
                _contentOfDrawingVisual.SetValue(drawingVisual, parentDrawingContent);

                DrawVisual(drawingContext, drawingVisual, parentRelatedXY, currentParent.RenderSize);
            }

            if (currentParent is Panel parentPanelToRender)
            {
                foreach (UIElement child in parentPanelToRender.Children)
                {
                    if (child == breakElement)
                    {
                        break;
                    }

                    var childRelatedXY = child.TranslatePoint(default, self);
                    var childRect = new Rect(childRelatedXY, child.RenderSize);

                    if (!selfRect.IntersectsWith(childRect))
                    {
                        continue; // skip if not intersecting
                    }

                    if (child.IsVisible)
                    {
                        DrawVisual(drawingContext, child, childRelatedXY, child.RenderSize);
                    }
                }
            }
        }
    }

    public static void DrawBackground(DrawingContext drawingContext, UIElement self)
    {
        var parentStack = new Stack&amp;lt;UIElement&amp;gt;();
        DrawBackground(drawingContext, self, parentStack, int.MaxValue, true);
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        DrawBackground(drawingContext, this, _parentStack, MaxDepth, false);
    }

    public int MaxDepth
    {
        get { return (int)GetValue(MaxDepthProperty); }
        set { SetValue(MaxDepthProperty, value); }
    }

    public static readonly DependencyProperty MaxDepthProperty =
        DependencyProperty.Register(&quot;MaxDepth&quot;, typeof(int), typeof(BackgroundPresenter), new PropertyMetadata(64));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;仓库&lt;/h2&gt;
&lt;p&gt;完整的测试代码仓库如下:&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;SlimeNull/BlurBehindTest&quot;}&lt;/p&gt;
&lt;p&gt;在之后, 我打算将其封装成更多易用的控件, 并添加到 WPF Suite 中. 敬请期待!&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;OrgEleCho/EleCho.WpfSuite&quot;}&lt;/p&gt;
</content:encoded></item><item><title>[WPF] 基于二阶系统的 ScrollViewer 平滑滚动</title><link>https://slimenull.com/posts/20250608004400/</link><guid isPermaLink="true">https://slimenull.com/posts/20250608004400/</guid><description>你在网上查到的大多是基于动画或者速度与加速度的仿真实现的平滑滚动, 但这里有基于自控二阶系统的仿真滚动</description><pubDate>Sun, 08 Jun 2025 00:44:00 GMT</pubDate><content:encoded>&lt;p&gt;如何让动画看起来真实? 用真实的方法控制它就好了&lt;/p&gt;
&lt;h2&gt;数据的平滑&lt;/h2&gt;
&lt;p&gt;在 YouTube 上, 有一位大佬做了一个这样的基于二阶系统的缓动: &lt;a href=&quot;https://www.youtube.com/watch?v=KPoeNZZ6H4s&quot;&gt;t3ssel8r - Giving Personality to Procedural Animations using Math&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;由此, 我们可以封装一个这样的数据平滑处理类:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;internal class SecondOrderDynamics
{
    private double _xp;// previous input
    private double _y, _yd; // state variables
    private double _w, _z, _d, k1, k2, k3; // dynamics constants
    private double _r;
    private double _f;

    /// &amp;lt;summary&amp;gt;
    /// 频率
    /// - 即速度, 单位是赫兹(Hz)
    /// - 不会影响输出结果的形状, 会影响 &apos;震荡频率&apos;
    /// &amp;lt;/summary&amp;gt;
    public double F
    {
        get =&amp;gt; _f; set
        {
            _f = value;
            InitMotionValues(_f, _z, _r);
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 阻尼 &amp;lt;br /&amp;gt;
    /// - 当为 0 时, 输出将永远震荡不衰减 &amp;lt;br /&amp;gt;
    /// - 当大于 0 小于 1 时, 输出会超出结果, 并逐渐趋于目标 &amp;lt;br /&amp;gt;
    /// - 当为 1 时, 输出的曲线是趋向结果, 并正好在指定频率对应时间内抵达结果 &amp;lt;br /&amp;gt;
    /// - 当大于 1 时, 输出值同样时取向结果, 但速度会更慢, 无法在指定频率对应时间内抵达结果 &amp;lt;br /&amp;gt;
    /// &amp;lt;/summary&amp;gt;
    public double Z
    {
        get =&amp;gt; _z; set
        {
            _z = value;
            InitMotionValues(_f, _z, _r);
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 初始响应
    /// - 当为 0 时, 数据需要进行 &apos;加速&apos; 来开始运动 &amp;lt;br /&amp;gt;
    /// - 当为 1 时, 数据会立即开始响应 &amp;lt;br /&amp;gt;
    /// - 当大于 1 时, 输出会因为 &apos;速度过快&apos; 而超出目标结果  &amp;lt;br /&amp;gt;
    /// - 当小于 0 时, 输出会 &apos;预测运动&apos;, 即 &apos;抬手动作&apos;. 例如目标是 &apos;加&apos; 时, 输出会先进行 &apos;减&apos;, 再进行 &apos;加&apos;, 
    /// - 当运动目标为机械时, 通常取值为 2
    /// &amp;lt;/summary&amp;gt;
    public double R
    {
        get =&amp;gt; _r; set
        {
            _r = value;
            InitMotionValues(_f, _z, _r);
        }
    }

    public SecondOrderDynamics(double f, double z, double r, double x0)
    {
        //compute constants
        InitMotionValues(f, z, r);

        // initialize variables
        _xp = x0;
        _y = x0;
        _yd = 0;
    }

    private void InitMotionValues(double f, double z, double r)
    {
        _w = 2 * Math.PI * f;
        _z = z;
        _d = _w * Math.Sqrt(Math.Abs(z * z - 1));
        k1 = z / (Math.PI * f);
        k2 = 1 / ((2 * Math.PI * f) * (2 * Math.PI * f));
        k3 = r * z / (2 * Math.PI * f);
    }

    public double Update(double deltaTime, double x, out double yd)
    {
        double xd = (x - _xp) / deltaTime;
        double k1_stable, k2_stable;

        if (_w * deltaTime &amp;lt; _z)
        {
            k1_stable = k1;
            k2_stable = Math.Max(Math.Max(k2, deltaTime * deltaTime / 2 + deltaTime * k1 / 2), deltaTime * k1);
        }
        else
        {
            double t1 = Math.Exp(-_z * _w * deltaTime);
            double alpha = 2 * t1 * (_z &amp;lt;= 1 ? Math.Cos(deltaTime * _d) : Math.Cosh(deltaTime * _d));
            double beta = t1 * t1;
            double t2 = deltaTime / (1 + beta - alpha);
            k1_stable = (1 - beta) * t2;
            k2_stable = deltaTime * t2;
        }

        _y = _y + deltaTime * _yd;
        _yd = _yd + deltaTime * (x + k3 * xd - _y - k1_stable * _yd) / k2_stable;

        _xp = x;

        yd = _yd;
        return _y;
    }

    public double Update(double deltaTime, double x) 
        =&amp;gt; Update(deltaTime, x, out _);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用它的方式, 就是逐帧的调用 Update 方法, deltaTime 传入距离上次调用所经过的时间, x 传入当前所需的值, 返回的结果则是经过平滑处理的数据.&lt;/p&gt;
&lt;p&gt;如果你用过 Unity, 那你一定知道 &lt;a href=&quot;https://docs.unity.cn/cn/current/ScriptReference/Mathf.SmoothDamp.html&quot;&gt;SmoothDamp&lt;/a&gt;, 在上面我们封装好的这个类中, 当 Z 是 1, R 是 0 时, 其呈现的效果, 与 SmoothDamp 一致.&lt;/p&gt;
&lt;p&gt;假设我们期望平滑后的数据在约莫 0.1s 后抵达目标值, 那么 F 应该传入 10.&lt;/p&gt;
&lt;h2&gt;在 WPF 中逐帧进行平滑&lt;/h2&gt;
&lt;p&gt;WPF 中, 每一帧进行渲染时, 都会触发 &lt;a href=&quot;https://learn.microsoft.com/zh-cn/dotnet/api/system.windows.media.compositiontarget.rendering&quot;&gt;CompositionTarget.Rendering&lt;/a&gt; 事件.&lt;/p&gt;
&lt;p&gt;需要注意的是, 为了避免内存泄漏, 我们应该在当前控件脱离可视化树的时候, 取消注册 Rendering 事件.&lt;/p&gt;
&lt;p&gt;有了这个, 我们只需要在鼠标进行滚动的时候, 记录目标值, 并且在每帧调用二阶系统缓动对数据做平滑, 再将 ScrollViewer 滚动到平滑后的位置即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;internal class SmoothScrollViewer : ScrollViewer
{
    protected override void OnVisualParentChanged(DependencyObject oldParent)
    {
        // 确保取消注册
        CompositionTarget.Rendering -= CompositionTargetRendering;

        if (VisualParent is not null)
        {
            // 如果新的 Parent 不为空, 也就是当前控件在可视化树上
            // 则注册 Rendering 事件

            CompositionTarget.Rendering += CompositionTargetRendering;
        }

        base.OnVisualParentChanged(oldParent);
    }

    private readonly Stopwatch _stopwatch = Stopwatch.StartNew();

    private double _scrollingTarget = double.NaN;       // 滚动目标
    private SecondOrderDynamics? _scrollingDynamics;    // 缓动对象
    private TimeSpan _lastRenderTime;                   // 上一次渲染时间

    private void CompositionTargetRendering(object? sender, EventArgs e)
    {
        var totalElapsed = _stopwatch.Elapsed;
        var elapsedSeconds = (totalElapsed - _lastRenderTime).TotalSeconds;
        _lastRenderTime = totalElapsed;

        if (double.IsNaN(_scrollingTarget))
        {
            _scrollingDynamics = null;
            return;
        }

        // 确保用于缓动对象已创建
        _scrollingDynamics ??= new(3, 1, 0, VerticalOffset);

        // 更新滚动值
        var animatedOffset = _scrollingDynamics.Update(elapsedSeconds, _scrollingTarget, out var yd);
        ScrollToVerticalOffset(animatedOffset);

        // 速度接近0, 结束滚动
        if (Math.Abs(yd) &amp;lt; 0.05)
        {
            _scrollingTarget = double.NaN;
            _scrollingDynamics = null;
        }
    }

    protected override void OnMouseWheel(MouseWheelEventArgs e)
    {
        // 如果当前没有滚动目标, 则设置新的滚动目标
        if (double.IsNaN(_scrollingTarget))
        {
            _scrollingTarget = VerticalOffset - e.Delta;
        }
        else
        {
            _scrollingTarget -= e.Delta;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果如下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/smooth_scrolling_with_two_order_system.gif&quot; alt=&quot;preview&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;使用 WPF Suite&lt;/h2&gt;
&lt;p&gt;上面已经实现了最简单的平滑滚动, 但是滚动也可能是横向的, 或者是由触摸板, 笔, 或者触摸设备引发的.&lt;/p&gt;
&lt;p&gt;而且要考虑到易用性, 还需要允许用户可以自定义平滑时间, 也需要能够通过属性来禁用平滑滚动行为.&lt;/p&gt;
&lt;p&gt;如果要全部考虑进去, 那么代码量就多了. 不妨试试我封装好的 ScrollViewer, 只需要安装 &lt;a href=&quot;https://www.nuget.org/packages/EleCho.WpfSuite&quot;&gt;EleCho.WpfSuite&lt;/a&gt; 包, 引入命名空间, 然后使用就可以了.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Window x:Class=&quot;SmoothScrollingTest.MainWindow&quot;
        xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
        xmlns:ws=&quot;https://schemas.elecho.dev/wpfsuite&quot;
        Title=&quot;MainWindow&quot; Height=&quot;450&quot; Width=&quot;800&quot;&amp;gt;
    &amp;lt;ws:ScrollViewer &amp;gt;
        &amp;lt;StackPanel x:Name=&quot;panel&quot; /&amp;gt;
    &amp;lt;/ws:ScrollViewer&amp;gt;
&amp;lt;/Window&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;::github{repo=&quot;OrgEleCho/EleCho.WpfSuite&quot;}&lt;/p&gt;
&lt;h2&gt;引用&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/TwilightLemon/p/18909374&quot;&gt;TwilightLemon - WPF 使用CompositionTarget.Rendering实现平滑流畅滚动的ScrollViewer，支持滚轮、触控板、触摸屏和笔&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=KPoeNZZ6H4s&quot;&gt;t3ssel8r - Giving Personality to Procedural Animations using Math&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[踩坑记录] DirectX 无法使用 Map 取得纹理数据</title><link>https://slimenull.com/posts/20250405205300/</link><guid isPermaLink="true">https://slimenull.com/posts/20250405205300/</guid><pubDate>Sat, 05 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;如果你需要对纹理调用 Map 方法, BindFlag 必须不能是非 0 值(被 Map 的纹理不能参与渲染), CPUAccessFlag 必须与你调用 Map 传入的 MapType 匹配.&lt;/p&gt;
&lt;p&gt;假设你需要读取一个不满足以上条件的纹理, 你需要创建一个新的纹理, 通过 CopySubresource 将数据拷贝到新纹理中, 再 Map 这个新纹理, 然后读取.&lt;/p&gt;
&lt;p&gt;参考资料: https://www.cnblogs.com/wickedpriest/p/13568190.html&lt;/p&gt;
</content:encoded></item><item><title>深入 WPF 列表虚拟化与树虚拟化</title><link>https://slimenull.com/posts/20241029142000/</link><guid isPermaLink="true">https://slimenull.com/posts/20241029142000/</guid><description>高性能 = ScrollViewer + ItemsControl + VirtualizingPanel</description><pubDate>Tue, 29 Oct 2024 14:20:00 GMT</pubDate><content:encoded>&lt;p&gt;在开发一些大型软件时, 不可避免的会遇到大量数据的呈现, 例如数万, 十几万甚至上百万个数据, 需要一个列表呈现.
倘若每一个数据都真正的在界面上创建对应的控件, 那电脑的性能不管如何, 都是不够的.&lt;/p&gt;
&lt;p&gt;这个时候, 就需要虚拟化了.&lt;/p&gt;
&lt;h2&gt;什么是虚拟化&lt;/h2&gt;
&lt;p&gt;简单来讲, 虚拟化就是仅呈现用户可见的数据. 不管如何, 数以万计的数据不可能同时都存在界面上, 在界面上, 一般用户都是只能看到几十条数据.&lt;/p&gt;
&lt;p&gt;在这种情况下, 只需要保证这几十条数据有对应的控件用于呈现, 就可以节省性能.
而当用户滚动的时候, 新进入可见区域的数据会创建对应控件, 离开可视区域的数据就会销毁对应控件, 以回收性能.&lt;/p&gt;
&lt;p&gt;当然, 除了每次滚动都需要新建与销毁控件, 也有另外一种重用控件的模式, 滚动出的内容会被滚动入内容所使用, 这样不需要新建控件, 内存稳定会好很多.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/virtualizing_tree_view.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;900 百万条数据, 树形结构, 在界面上呈现, 最终内存占用只有 1G 多些. 其中 400MB 是 WPF 的基础占用.&lt;/p&gt;
&lt;h2&gt;使用虚拟化&lt;/h2&gt;
&lt;p&gt;WPF 内置了许多自带虚拟化的控件, ListBox, ListView, GridView, TreeView 都是可以直接使用的.&lt;/p&gt;
&lt;p&gt;给 ItemsSource 属性设置数据源之后, 上述控件即可自动启用虚拟化.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/virtualizing_list_box.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果要禁用虚拟化, 只需要在对应容器控件上设置附加属性 VirtualizingPanel.IsVirtualizing 为 false 即可.&lt;/p&gt;
&lt;h2&gt;虚拟化的不同模式&lt;/h2&gt;
&lt;p&gt;在默认模式下, 离开可视区域的数据对应的控件会被销毁.
通过设置容器的 &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.virtualizationmode&quot;&gt;VirtualizingPanel.VirtualizationMode&lt;/a&gt; 附加属性可以更改此行为.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard (默认)&lt;/td&gt;
&lt;td&gt;创建并且销毁对应控件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recycling&lt;/td&gt;
&lt;td&gt;重用对应控件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;虚拟化时不同的滚动模式&lt;/h2&gt;
&lt;p&gt;在开启虚拟化时, 每次进行滚动, 默认最小的单位就不再是原来的像素, 而是一项数据.
通过设置容器的 &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.scrollunit&quot;&gt;VirtualizingPanel.ScrollUnit&lt;/a&gt; 附加属性可以更改此行为.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pixel&lt;/td&gt;
&lt;td&gt;最小的滚动单位是像素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Item (默认)&lt;/td&gt;
&lt;td&gt;最小的滚动单位是一项数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;虚拟化的实现要求&lt;/h2&gt;
&lt;p&gt;WPF 自 .NET Framework 4.5 开始, 就提供了比较完善的虚拟化支持.&lt;/p&gt;
&lt;p&gt;要实现虚拟化, 你需要有以下元素:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ScrollViewer, 用于处理滚动, 以及可见区域的计算, 并确保其 CanContentScroll 设为 true (默认是 false, 需手动设置为 true)&lt;/li&gt;
&lt;li&gt;ItemsControl, 用于根据数据生成对应控件, 并确保启用虚拟化 (默认启用)&lt;/li&gt;
&lt;li&gt;VirtualizingPanel, 适用于虚拟化后的界面的布局&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;并且, 必须保证有以下层级关系:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ScrollContentPresenter
|- ItemsPresenter
    |- VirtualizingPanel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体的操作方式:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;重写 ItemsControl 的模板, 保证模板内包含一个 ScrollViewer, 且是 ItemsPresenter 的直接父节点, 并设置 ScrollViewer 的 CanContentScroll 为 True&lt;pre&gt;&lt;code&gt;&amp;lt;ScrollViewer CanContentScroll=&quot;True&quot;&amp;gt;
    &amp;lt;ItemsPresenter/&amp;gt;
&amp;lt;/ScrollViewer&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;保证 ItemsControl 的 ItemsPanel 是一个虚拟化容器&lt;pre&gt;&lt;code&gt;&amp;lt;ItemsControl&amp;gt;
    &amp;lt;ItemsControl.ItemsPanel&amp;gt;
        &amp;lt;ItemsPanelTemplate&amp;gt;
            &amp;lt;VirtualizingStackPanel /&amp;gt;
        &amp;lt;/ItemsPanelTemplate&amp;gt;
    &amp;lt;/ItemsControl.ItemsPanel&amp;gt;
&amp;lt;/ItemsControl&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;关于内置虚拟化容器, ListBox, ListView 这些控件的模板中自带 ScrollViewer, 而且它们的 ItemsPanel 默认就是 VirtualizingStackPanel, 所以无需更改就直接能进行虚拟化&lt;/p&gt;
&lt;h2&gt;树形结构虚拟化&lt;/h2&gt;
&lt;p&gt;在 WPF 的 TreeView 控件中, 主要需要使用 TreeView 和 TreeViewItem 这两个. 但其实, 它们都是一种 &quot;列表容器&quot;. TreeView 和 TreeViewItem 都直接或间接的继承自 ItemsControl.&lt;/p&gt;
&lt;p&gt;TreeViewItem 继承自 HeaderedItemsControl, 而 HeaderedItemsControl 相比较 ItemsControl, 就是多了 &lt;code&gt;Header&lt;/code&gt;, &lt;code&gt;HeaderTemplate&lt;/code&gt;, &lt;code&gt;HeaderTemplateSelector&lt;/code&gt; 这几个属性.&lt;/p&gt;
&lt;p&gt;TreeViewItem 的模板大致如下, 本质就是在列表的最上方, 加了一个标头:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Grid&amp;gt;
    &amp;lt;Grid.ColumnDefinitions&amp;gt;
        &amp;lt;ColumnDefinition MinWidth=&quot;19&quot; Width=&quot;Auto&quot;/&amp;gt;
        &amp;lt;ColumnDefinition Width=&quot;Auto&quot;/&amp;gt;
        &amp;lt;ColumnDefinition Width=&quot;*&quot;/&amp;gt;
    &amp;lt;/Grid.ColumnDefinitions&amp;gt;
    &amp;lt;Grid.RowDefinitions&amp;gt;
        &amp;lt;RowDefinition Height=&quot;Auto&quot;/&amp;gt;
        &amp;lt;RowDefinition/&amp;gt;
    &amp;lt;/Grid.RowDefinitions&amp;gt;
    &amp;lt;ToggleButton x:Name=&quot;Expander&quot; ClickMode=&quot;Press&quot; IsChecked=&quot;{Binding IsExpanded, RelativeSource={RelativeSource Mode=TemplatedParent}}&quot; Style=&quot;{StaticResource ExpandCollapseToggleStyle}&quot;/&amp;gt;
    &amp;lt;Border x:Name=&quot;Bd&quot; Background=&quot;{TemplateBinding Background}&quot; BorderBrush=&quot;{TemplateBinding BorderBrush}&quot; BorderThickness=&quot;{TemplateBinding BorderThickness}&quot; Grid.Column=&quot;1&quot; Padding=&quot;{TemplateBinding Padding}&quot; SnapsToDevicePixels=&quot;true&quot;&amp;gt;
        &amp;lt;ContentPresenter x:Name=&quot;PART_Header&quot; ContentSource=&quot;Header&quot; HorizontalAlignment=&quot;{TemplateBinding HorizontalContentAlignment}&quot; SnapsToDevicePixels=&quot;{TemplateBinding SnapsToDevicePixels}&quot;/&amp;gt;
    &amp;lt;/Border&amp;gt;
    &amp;lt;ItemsPresenter x:Name=&quot;ItemsHost&quot; Grid.Column=&quot;1&quot; Grid.ColumnSpan=&quot;2&quot; Grid.Row=&quot;1&quot;/&amp;gt;
&amp;lt;/Grid&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但能使 TreeViewItem 递归性参与 TreeView 的虚拟化的原因, 还是 &lt;a href=&quot;https://learn.microsoft.com/zh-cn/dotnet/api/system.windows.controls.primitives.ihierarchicalvirtualizationandscrollinfo&quot;&gt;IHierarchicalVirtualizationAndScrollInfo&lt;/a&gt; 这个接口.&lt;/p&gt;
&lt;p&gt;在 VirtualizingStackPanel 进行虚拟化布局时, 会递归性的判断容器内的元素是否实现此接口, 并让其参与到虚拟化布局中.&lt;/p&gt;
&lt;p&gt;不过, 通过查看 &lt;a href=&quot;https://source.dot.net/#PresentationFramework/System/Windows/Controls/TreeViewItem.cs,c17a2c09aaa80559&quot;&gt;TreeViewItem 的源代码&lt;/a&gt;, 我们可以知道, 实现此接口并不简单, 在 WPF 自己对 TreeViewItem 的实现中, 使用了大量 internal 的代码.&lt;/p&gt;
&lt;h2&gt;提示与注意事项&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;VirtualizingPanel.IsVirtualizing 属性是设置在 ItemsControl 上的, 也就是最外层的, 诸如 ListBox, ListView 的容器. 将它设置在 VirtualizingPanel 上不会有任何作用. 且默认的你不需要更改此值, 因为此值默认就为 true&lt;/li&gt;
&lt;li&gt;CanContentScroll 应该设置到 ScrollViewer 上. 此属性不会自动从父节点继承, 你应该直接在模板内就为它设置好 true, 因为此值默认为 false&lt;/li&gt;
&lt;li&gt;在模板中, ScrollViewer 和 ItemsPresenter 之间不能加任何节点, 否则都会导致虚拟化失效. 这意味着你不能直接不改模板, 而是直接在 ItemsControl 外面套一个 ScrollViewer 草草了事. 你只能通过更改模板来为它添加 ScrollViewer.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>并非类型前置, 有趣的 C 语言变量声明, 函数声明与函数指针声明</title><link>https://slimenull.com/posts/202410070406/</link><guid isPermaLink="true">https://slimenull.com/posts/202410070406/</guid><description>C 语言并非类型前置, 也更不可能是类型后置, 它更像是... 类型乱置</description><pubDate>Mon, 07 Oct 2024 04:06:00 GMT</pubDate><content:encoded>&lt;p&gt;如果你学过多门现代编程语言, 比如 C#, F#, Golang, Rust 这些,
那你一定知道, 目前越来越多的新语言正使用着一种叫做 &quot;类型后置&quot; 的变量与函数声明风格.&lt;/p&gt;
&lt;h2&gt;何为类型后置&lt;/h2&gt;
&lt;p&gt;在 C# 中, 声明一个 int 类型, 名为 count 的变量, 其写法如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int count;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中, 类型在前, 名称在后, 这就是类型前置. 而在 Golang 中, 同样的声明就变成了这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var count int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时, 名称在前, 类型灾后, 这就是类型后置. 其它很多种语言都是类型后置, 例如 F#, Rust, Kotlin:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let count : int = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;let count : i32
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;var count : Int
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;不属于类型前置或后置的 C 语言&lt;/h2&gt;
&lt;p&gt;有很多人都将 C 语言误认为是类型前置, 毕竟 C# 和 Java 这类 &quot;C 族&quot; 语言中, 都是类型前置, 但其实 C 并不是.&lt;/p&gt;
&lt;p&gt;在 C 语言中, 声明一个整数型 count 的写法如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int count;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;怪异的数组&lt;/h3&gt;
&lt;p&gt;但是, 如果要声明一个整形数组, 语法就开始怪异了起来:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int numbers[10];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上述代码中, 表示 &quot;数组&quot; 的中括号, 并不在类型旁边, 甚至都不在名称的前面. 比起 &quot;类型&quot;, 这里的中括号更像是对 numbers 这个变量的一种 &quot;修饰&quot;.&lt;/p&gt;
&lt;p&gt;而在 C# 中, 声明一个 int 数组, 却是实实在在的类型前置:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int[] numbers;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里不讨论 C# 的 &quot;本地数组&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;怪异的指针&lt;/h3&gt;
&lt;p&gt;很多人在 C 语言中声明指针类型变量, 都喜欢这么写:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int* pointer;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但其实这并不算最正确的写法. 假如你希望声明两个指针, 如果你像下面这样写的话, 会怎样?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int* pointer1, pointer2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你会发现, pointer2 根本不是一个指针, 而是一个普普通通的 int. 而上述语法的最正确写法其实是:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int *pointer1, pointer2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你应该发现了, 表示指针的 &quot;星号&quot; 并没有和 int 结合成一个整体, 并作为后面两个变量的类型,
而是对 &lt;code&gt;pointer1&lt;/code&gt; 进行修饰, 使它成为一个指针. 至于 &lt;code&gt;pointer2&lt;/code&gt;, 它没有被修饰, 自然不是指针.&lt;/p&gt;
&lt;h2&gt;函数指针&lt;/h2&gt;
&lt;p&gt;C 语言中, 函数指针应该是让很多人都感到头痛的一个东西.&lt;/p&gt;
&lt;h3&gt;简单指针&lt;/h3&gt;
&lt;p&gt;下面是一个简单的, 传入两个 int, 传出一个 int 的函数指针:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int (*func)(int, int);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么这里 &lt;code&gt;*func&lt;/code&gt; 需要使用括号括住呢? 假如我们去掉他, 会发生什么?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int *func(int, int);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它变成了一个函数声明! 这里其实可以理解为, 名称 &lt;code&gt;func&lt;/code&gt; 没有优先与星号结合, 而是和后面的括号结合, 变成了函数声明.
之后再和型号结合, 表示此函数返回一个指针. 而最开始的 int, 正是指针的类型.&lt;/p&gt;
&lt;p&gt;其实, 在声明函数指针的时候, 我们还可以为参数命名. 与其说 &quot;还可以为参数命名&quot;, 不如说不给参数命名, 其实是省略:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int (*func)(int a, int b);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而删去参数 &lt;code&gt;a&lt;/code&gt; &lt;code&gt;b&lt;/code&gt; 这两个名称, 就成了我们经常见到的, 最为精简的函数指针声明.&lt;/p&gt;
&lt;h3&gt;参数为函数指针的函数指针&lt;/h3&gt;
&lt;p&gt;那么假如, 我需要声明一个函数指针 func2, 它传入一个参数为两个 int, 返回值为 int 的函数指针, 还传入两个 int, 该怎么写? 大概是这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int (*func2)(函数指针, int, int);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后, 这个函数指针的第一个参数所需要的函数指针, 如果要声明一个这样的变量, 应该是这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int (*param1)(int, int);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后, 我们直接将 &lt;code&gt;param1&lt;/code&gt; 直接塞到 &lt;code&gt;func2&lt;/code&gt; 的参数列表中, 变成这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// int (*func2)(函数指针, int, int);
// 塞入 int (*param1)(int, int);

int (*func2)(int (*param1)(int, int), int, int);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;省略掉参数名称 &lt;code&gt;param1&lt;/code&gt;, 就变成了这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int (*func2)(int (*)(int, int), int, int);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此, 声明完成.&lt;/p&gt;
&lt;h3&gt;返回值为普通指针的函数指针&lt;/h3&gt;
&lt;p&gt;假如我需要声明一个函数指针, 它传入两个 int, 并返回一个 int 指针, 其声明方式也非常简单.&lt;/p&gt;
&lt;p&gt;首先写一个传入两个 int, 传出一个 int 的函数指针:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int (*func)(int, int);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用 * 对声明进行修饰, 表示函数指针的返回值是指针:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int *(*func)(int, int);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;声明完毕&lt;/p&gt;
&lt;h3&gt;返回值为函数指针的函数指针&lt;/h3&gt;
&lt;p&gt;假如我需要声明一个函数指针, 它传入两个 int, 返回一个函数指针. 返回的函数指针需要传入两个 int, 并返回一个 int.&lt;/p&gt;
&lt;p&gt;根据上述说明, 我们可以按照非常简单明了的方式进行编写:&lt;/p&gt;
&lt;p&gt;首先, 最终的返回值, 也就是返回的函数指针的返回值, 是 int&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们需要声明的是一个指针:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int *func
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它是一个函数指针, 并需要传入两个 int:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int (*func)(int, int)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个函数指针, 返回的也是一个指针:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int *(*func)(int, int)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回的函数指针, 需要传入两个 int:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int (*(*func)(int, int))(int, int);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此, 声明完成.&lt;/p&gt;
&lt;h3&gt;参数和返回值均为函数指针&lt;/h3&gt;
&lt;p&gt;在上面我们已经有了返回值为函数指针的函数指针了:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int (*(*func)(int, int))(int, int);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们完全可以把任何一个函数指针的声明加到参数中, 这样它的参数就多了一个函数指针:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int (*(*func)(int (*)(int, int), int, int))(int, int);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然, 你也可以往返回的函数指针的参数中加函数指针:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int (*(*func)(int (*)(int, int), int, int))(int (*)(int, int), int, int);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在这个东西看起来已经很复杂了, 其实正常使用根本不会声明这么复杂的函数指针, 就算需要声明复杂的函数指针, 也会借助 &lt;code&gt;typedef&lt;/code&gt; 来声明.&lt;/p&gt;
&lt;h2&gt;类型声明&lt;/h2&gt;
&lt;p&gt;类型声明其实就是在变量声明前面加 &lt;code&gt;typedef&lt;/code&gt; 关键字, 使其变成了类型声明.&lt;/p&gt;
&lt;h3&gt;简单类型声明&lt;/h3&gt;
&lt;p&gt;例如, 我们声明一个 int 类型的变量, 名为 count:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int count;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在给它加上 &lt;code&gt;typedef&lt;/code&gt; 关键字:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef int count;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在, count 就表示一个 int.&lt;/p&gt;
&lt;h3&gt;函数指针类型声明&lt;/h3&gt;
&lt;p&gt;同理, 如果是函数指针:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int (*func_int_int_int)(int, int);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里, func_int_int_int 表示一个传入两个 int, 传出一个 int 的函数指针变量&lt;/p&gt;
&lt;p&gt;现在给它加上 &lt;code&gt;typedef&lt;/code&gt; 关键字:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef int (*func_int_int_int)(int, int);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是 func_int_int_int 就变成传入两个 int, 传出一个 int 的函数指针了.&lt;/p&gt;
&lt;p&gt;如果我们需要声明其他的, 传入两个 int, 传出一个 int 的函数指针变量, 当然可以借助刚刚声明好的类型:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func_int_int_int fp1;
func_int_int_int fp2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说, 变量声明语句, 加上 &lt;code&gt;typedef&lt;/code&gt; 关键字, 原来是什么类型的变量, 就变成了什么类型的别名.&lt;/p&gt;
&lt;h2&gt;函数声明&lt;/h2&gt;
&lt;p&gt;现在, 我们知道了, 在 C 语言中, 定义复杂类型变量, 其实就是对变量进行不断修饰的一个过程. 那么, 函数呢?&lt;/p&gt;
&lt;p&gt;假如我要声明一个无参数, 返回值为整形指针的函数, 应该怎么写? &lt;code&gt;int* func()&lt;/code&gt; 还是 &lt;code&gt;int *func()&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;那么最后的答案是... 后者. 至于为什么, 只需要看了这个声明, 你就知道:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int(*func()) {
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尽管我给 &lt;code&gt;*func()&lt;/code&gt; 加上了括号, 但是它仍然是一个正确的函数定义. 这意味着, 星号在没有使用括号的情况下, 就是与后面的部分结合的.
而既然它并不是与前面的 int 类型结合, 那在写的时候, 当然也不应该将星号和 int 写在一起.&lt;/p&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;在偶然的机会下, 我和朋友聊到了上面提到的这些东西, 并在之后写下了本篇文章.&lt;/p&gt;
&lt;p&gt;毕竟我 C/C++ 的熟练程度其实是远不如 C# 的. 在最初学 C 语言, 看到 C 语言声明各种复杂类型变量的时候, 我的内心是懵逼的.
直到后来, 对编译原理的了解加深了, 再后来, 偶然重新捡起 C/C++ 的这些东西, 才恍然大悟.&lt;/p&gt;
&lt;p&gt;其实到现在, 我也有很多疑惑. C 语言的语法解析器, 到底是如何设计, 才会导致函数指针变量声明需要加括号的这种行为呢?&lt;/p&gt;
&lt;p&gt;&lt;code&gt;*func()&lt;/code&gt; 在不加括号的情况下, func 会向后结合括号, 而没有向前结合星号. 在我看来, 这属实是很奇怪的语法. 不过, 我现在对于语法解析器了解的也不多, 不懂这种行为, 也实属正常.&lt;/p&gt;
</content:encoded></item><item><title>如何优雅的在一个解决方案内使 C# 项目引用 C++ 库项目</title><link>https://slimenull.com/posts/202410060603/</link><guid isPermaLink="true">https://slimenull.com/posts/202410060603/</guid><description>指定 ProjectReference 的 ReferenceOutputAssembly 为 false, 并使用自定义 Target, 并添加 Copy 项以自动拷贝库构建输出</description><pubDate>Sun, 06 Oct 2024 06:03:00 GMT</pubDate><content:encoded>&lt;p&gt;如果在一个解决方案内, 有一个 C# 项目, 需要使用另一个 C++ 库项目构建出的 dll...&lt;/p&gt;
&lt;p&gt;本文章示例代码已上传至 GitHub 仓库: &lt;a href=&quot;https://github.com/SlimeNull/CSharpUseCppLibraryProject&quot;&gt;SlimeNull/CSharpUseCppLibraryProject&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意, 此文章中需要使用的 C# 项目, 最好是 SDK 样式 csproj, 参考:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/dotnet/core/porting/upgrade-assistant-overview&quot;&gt;.NET 升级助手概述 - .NET Core | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/63055430/how-to-create-an-sdk-style-net-framework-project-in-vs&quot;&gt;如何在 VS 中创建 SDK 样式的 .NET Framework 项目&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;不太方便的常见手动方案&lt;/h2&gt;
&lt;p&gt;常见的手动方案有以下几种:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;手动构建 C++ 项目, 并将其复制到 C# 项目内, 然后指定自动拷贝&lt;/li&gt;
&lt;li&gt;手动构建 C++ 项目, 并将其复制到 C# 项目的构建输出目录, 以使 C# 项目在运行时不出问题&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一种方案会导致二进制文件直接被包含到项目中, 这意味着, 如果你的仓库使用了版本控制, 那么每一次该二进制文件变更,
都会在版本控制中留下历史记录. 而且考虑到二进制文件的大小, 这也会使仓库的空间占用大大增加.&lt;/p&gt;
&lt;p&gt;第二种方案不会污染版本控制, 但是如果一个人刚接触此项目, 他可能并不知道需要将 C++ 库项目的构建输出复制到 C# 项目的构建输出,
然后在 C# 项目运行崩溃时感到疑惑.&lt;/p&gt;
&lt;p&gt;当然, 第二种方案你也可以在项目中留下 readme, 并在其写明构建流程, 但这无疑是使构建流程更加复杂了.&lt;/p&gt;
&lt;h2&gt;行不通的预构建与构建后事件&lt;/h2&gt;
&lt;p&gt;我们知道, 使用 Visual Studio 构建项目, 可以指定 &quot;预构建事件&quot; 和 &quot;构建后事件&quot;,
或许我们可以通过在其中写指令, 指定在构建 C# 项目前, 先构建 C++ 项目, 在构建后再将 C++ 项目的构建输出自动拷贝到 C# 项目构建输出目录&lt;/p&gt;
&lt;p&gt;假如我们使用 MSBuild 直接构建 vcproj&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MSBuild TestDll.vcproj
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;要使用 MSBuild 指令, 你需要在开始菜单中, 打开 &quot;VS 2022 开发者命令行&quot;, 或者手动配置 PATH 环境变量.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;你会发现, 构建输出并不在解决方案下的 &quot;x64/Debug&quot; 目录下, 而是在项目文件夹下的 &quot;Debug&quot; 目录下.
这和 Visual Studio 的构建行为不一致.&lt;/p&gt;
&lt;p&gt;而且, 如果你真的在预构建事件中执行 MSBuild 指令, 你会发现, 它根本找不到这个指令, 这说明在构建时, PATH 环境变量中找不到 MSBuild 这个可执行文件.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/MSBuild_not_found.png&quot; alt=&quot;MSBuild 找不到&quot; /&gt;&lt;/p&gt;
&lt;p&gt;再考虑到构建者的 MSBuild 位置是不确定的, 所以, 在预构建与构建后事件中使用 MSBuild 的这条路堵死了.&lt;/p&gt;
&lt;h2&gt;项目引用自定义目标&lt;/h2&gt;
&lt;p&gt;如果你曾阅读过 csproj, 那么你肯定知道, C# 项目的项目引用, 是通过 ItemGroup 下的 ProjectReference 实现的.
其实, 我们也可以通过 ProjectReference 来引用 C++ 项目.&lt;/p&gt;
&lt;p&gt;右键项目, 添加项目引用, 并勾选 C++ 项目然后确认, 或者直接修改 csproj, 添加 ProjectReference 之后,
你会在 C# 项目中的 &quot;依赖项&quot; 下看到一个警告.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/WarningWhenCSharpReferenceCppProject.png&quot; alt=&quot;C# 项目引用 C++ 项目的警告&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这是因为 C# 不存在 &quot;引用&quot; C++ 程序集的说法, 打开 csproj, 并在对应 ProjectReference 下添加 &lt;code&gt;ReferenceOutputAssembly=&quot;False&quot;&lt;/code&gt; 即可消除此警告.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ProjectReference Include=&quot;..\TestDll\TestDll.vcxproj&quot; ReferenceOutputAssembly=&quot;False&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在, 当构建 C# 项目的时候, 指定的 C++ 项目 TestDll 就会被自动构建. 不过我们还需要添加自定义 &quot;Target&quot; 以自动拷贝 C++ 项目的构建输出到 C# 项目输出目录.&lt;/p&gt;
&lt;p&gt;打开项目 csproj, 并在 &lt;code&gt;Project&lt;/code&gt; 节点下添加以下代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ItemGroup&amp;gt;
  &amp;lt;DllFiles Include=&quot;../x64/$(Configuration)/*.dll;../x64/$(Configuration)/*.pdb&quot; /&amp;gt;
&amp;lt;/ItemGroup&amp;gt;

&amp;lt;Target Name=&quot;CopyDLL&quot; AfterTargets=&quot;Build&quot;&amp;gt;
  &amp;lt;Copy SourceFiles=&quot;@(DllFiles)&quot; DestinationFolder=&quot;$(OutDir)&quot; /&amp;gt;
&amp;lt;/Target&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ItemGroup 下的 DllFiles 表示 C++ 项目下输出的 dll 和 pdb 文件,
这里 DllFiles 名称可以换成其他任意自定义名称, &lt;code&gt;$(Configuration)&lt;/code&gt; 则表示当前构建配置&lt;/li&gt;
&lt;li&gt;下面的 Target 则是实际进行拷贝的目标了, &lt;code&gt;AfterTargets=&quot;Build&quot;&lt;/code&gt; 表示它在构建之后执行.
SourceFiles 这里直接引用前面定义的 DllFiles, DestinationFolder 则是当前项目的构建输出目录, 使用 $(OutDir) 进行引用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;完整供参考的 csproj 如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&amp;gt;

  &amp;lt;PropertyGroup&amp;gt;
    &amp;lt;OutputType&amp;gt;Exe&amp;lt;/OutputType&amp;gt;
    &amp;lt;TargetFramework&amp;gt;net8.0&amp;lt;/TargetFramework&amp;gt;
    &amp;lt;ImplicitUsings&amp;gt;enable&amp;lt;/ImplicitUsings&amp;gt;
    &amp;lt;Nullable&amp;gt;enable&amp;lt;/Nullable&amp;gt;
  &amp;lt;/PropertyGroup&amp;gt;

  &amp;lt;ItemGroup&amp;gt;
    &amp;lt;ProjectReference Include=&quot;..\TestDll\TestDll.vcxproj&quot; ReferenceOutputAssembly=&quot;False&quot; /&amp;gt;
  &amp;lt;/ItemGroup&amp;gt;

  &amp;lt;ItemGroup&amp;gt;
    &amp;lt;DllFiles Include=&quot;../x64/$(Configuration)/*.dll;../x64/$(Configuration)/*.pdb&quot; /&amp;gt;
  &amp;lt;/ItemGroup&amp;gt;

  &amp;lt;Target Name=&quot;CopyDLL&quot; AfterTargets=&quot;Build&quot;&amp;gt;
    &amp;lt;Copy SourceFiles=&quot;@(DllFiles)&quot; DestinationFolder=&quot;$(OutDir)&quot; /&amp;gt;
  &amp;lt;/Target&amp;gt;

&amp;lt;/Project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时, 当你构建 C# 项目, C++ 项目就会自动构建, 并拷贝构建输出到 C# 项目输出目录.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/BuildCSharpProjectWithCppProjectReference.png&quot; alt=&quot;构建带有 C++ 项目引用的 C# 项目&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>通过点坐标, 反求贝塞尔曲线对应的时间参数</title><link>https://slimenull.com/posts/202410030450/</link><guid isPermaLink="true">https://slimenull.com/posts/202410030450/</guid><description>贝塞尔曲线在二维上的算法, 可以分解成两个相同的一维上的算法. 而任意阶的贝塞尔曲线都可以写作一个方程. 有方程, 就能尝试求解.</description><pubDate>Thu, 03 Oct 2024 04:50:00 GMT</pubDate><content:encoded>&lt;p&gt;贝塞尔曲线在二维上的算法, 可以分解成两个相同的一维上的算法. 而任意阶的贝塞尔曲线都可以写作一个方程. 有方程, 就能尝试求解.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文所有代码均已上传至 GitHub 仓库: &lt;a href=&quot;https://github.com/SlimeNull/SolveBezierCurve&quot;&gt;SlimeNull/SolveBezierCurve&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;算法概述&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/bezier2.png&quot; alt=&quot;bezier2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;以二阶贝塞尔曲线开始, 它有三个控制点, 构成了两个线段. 计算指定时间 t 上的点, 步骤如下:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在两个线段上, 取得进度为 t 的点, 得到两个点, 即一个线段&lt;/li&gt;
&lt;li&gt;在一个线段上, 取得进度为 t 的点, 得到一个点, 即结果.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里说的 &quot;进度为 t 的点&quot;, 则是对两点进行插值, t 为参数. 当 t 越接近 0 时, 结果越接近起始点. 当 t 越接近 1 时, 结果越接近结束点.&lt;/p&gt;
&lt;p&gt;写成 C# 代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义表示 Point 的结构体
public record struct Point(double X, double Y);

public class BezierUtilities
{
    // 对 double 进行插值的函数
    public static double Lerp(double a, double b, double t)
    {
        return a * (1 - t) + b * t;
    }

    // 对 Point 进行插值的函数
    public static Point Lerp(Point a, Point b, double t)
    {
        return new Point(Lerp(a.X, b.X, t), Lerp(a.Y, b.Y, t));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同理, 三阶贝塞尔曲线由四个控制点, 也就是三个线段构成, 其计算过程如下:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在三个线段上, 取得进度为 t 的点, 得到三个点, 即两个线段&lt;/li&gt;
&lt;li&gt;在两个线段上, 取得进度为 t 的点, 得到两个点, 即一个线段&lt;/li&gt;
&lt;li&gt;在一个线段上, 取得进度为 t 的点, 得到一个点, 即结果.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以看到, 不论多少阶的贝塞尔曲线, 其算法都是单调的&lt;/p&gt;
&lt;h2&gt;逻辑拆解&lt;/h2&gt;
&lt;p&gt;通过前面的插值算法可以知道, 贝塞尔曲线不同维度上的计算结果互不影响, 也就是说, 我们完全可以将二维上的二阶贝塞尔曲线, 降维到一维.
这样, 原本由两个值构成的 &quot;控制点&quot;, 现在变成了纯数字.&lt;/p&gt;
&lt;h3&gt;二阶贝塞尔曲线&lt;/h3&gt;
&lt;p&gt;那么接下来, 设一维二阶贝塞尔曲线三个控制点的值分别为 a, b, c , 时间参数为 t, 计算逻辑如下:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;计算线段 a-b 之间的插值点 $ d = a \times (1 - t) + b \times t $ &lt;br /&gt;
计算线段 b-c 之间的插值点 $ e = b \times (1 - t) + c \times t $&lt;/li&gt;
&lt;li&gt;计算线段 d-e 之间的插值点, 即最终的插值点: $ r = d \times (1 - t) + e \times t $&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;将它们展开, 最终的计算公式是:&lt;/p&gt;
&lt;p&gt;$
r = (a \times (1 - t) + b \times t) \times (1 - t) + (b \times (1 - t) + c \times t) \times t
$&lt;/p&gt;
&lt;p&gt;即:&lt;/p&gt;
&lt;p&gt;$
r = a t^2 - 2 a t + a - 2 b t^2 + 2 b t + c t^2
$&lt;/p&gt;
&lt;p&gt;对于编程中的 &quot;动画&quot; 所使用的贝塞尔曲线, a 为 0, c 为 1, 此时公式是:&lt;/p&gt;
&lt;p&gt;$
r = - 2 b t^2 + 2 b t + t^2
$&lt;/p&gt;
&lt;p&gt;当 b 值确定时, 这就是一个简单的一元二次方程. 将其化为标准形式:&lt;/p&gt;
&lt;p&gt;$
(- 2b + 1) t^2 + 2 b t - r = 0
$&lt;/p&gt;
&lt;p&gt;随便扔给 &lt;a href=&quot;https://www.wolframalpha.com/input?i=%28-2b+%2B+1%29+t%5E2+%2B+2+bt+-+r+%3D+0%2C+t+%3D+%3F&quot;&gt;WolframAlpha&lt;/a&gt; 就能得出:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 b 为 0.5 时, 此时解为: $ t = r $&lt;/li&gt;
&lt;li&gt;当 b 不为 0.5 时, 解: $ r = \frac{b \pm \sqrt{b^2 - 2br + r}}{2b - 1} $&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;三阶贝塞尔曲线&lt;/h3&gt;
&lt;p&gt;对于三阶贝塞尔曲线,&lt;/p&gt;
&lt;p&gt;那么接下来, 设一维二阶贝塞尔曲线三个控制点的值分别为 a, b, c , 时间参数为 t, 计算逻辑如下:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;计算线段 a-b 之间的插值点 $ e = a \times (1 - t) + b \times t $ &lt;br /&gt;
计算线段 b-c 之间的插值点 $ f = b \times (1 - t) + c \times t $ &lt;br /&gt;
计算线段 c-d 之间的插值点 $ g = c \times (1 - t) + d \times t $&lt;/li&gt;
&lt;li&gt;计算线段 e-f 之间的插值点 $ h = e \times (1 - t) + f \times t $ &lt;br /&gt;
计算线段 f-g 之间的插值点 $ i = f \times (1 - t) + g \times t $&lt;/li&gt;
&lt;li&gt;计算线段 h-i 之间的插值点, 即最终的插值点: $ r = h \times (1 - t) + i \times t $&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;将它们展开, 最终的计算公式是:&lt;/p&gt;
&lt;p&gt;$
r = ((a \times (1 - t) + b \times t) \times (1 - t) + (b \times (1 - t) + c \times t) \times t) \times (1 - t) + ((b \times (1 - t) + c \times t) \times (1 - t) + (c \times (1 - t) + d \times t) \times t) \times t
$&lt;/p&gt;
&lt;p&gt;即:&lt;/p&gt;
&lt;p&gt;$
r = -a t^3 + 3 a t^2 - 3 a t + a + 3 b t^3 - 6 b t^2 + 3 b t - 3 c t^3 + 3 c t^2 + d t^3
$&lt;/p&gt;
&lt;p&gt;对于编程中的 &quot;动画&quot; 所使用的贝塞尔曲线, a 为 0, d 为 1, 此时公式是:&lt;/p&gt;
&lt;p&gt;$
r = 3 b t^3 - 6 b t^2 + 3 b t - 3 c t^3 + 3 c t^2 + t^3
$&lt;/p&gt;
&lt;p&gt;当 b 值确定时, 这就是一个简单的一元二次方程. 将其化为标准形式:&lt;/p&gt;
&lt;p&gt;$
(3b - 3c + 1)t^3 + (-6b + 3c)t^2 + 3bt - r = 0
$&lt;/p&gt;
&lt;p&gt;当然, 三次方程的求根公式就比二次方程的复杂多了, 在这就不写出来了. 在下一部分中给出的逻辑代码, 使用的是盛金法求三次方程.&lt;/p&gt;
&lt;h3&gt;选取合适的根&lt;/h3&gt;
&lt;p&gt;对于贝塞尔曲线, t 的值应该总是大于等于 0, 小于等于 1 的. 所以在解方程之后, 不在此范围的根应该抛弃掉.&lt;/p&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;p&gt;简单抽象一个 &quot;点&quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public record struct Point(double X, double Y)
{
    public bool IsEmpty
    {
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        get =&amp;gt; Is(0, 0);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool Is(double x, double y) =&amp;gt; X == x &amp;amp;&amp;amp; Y == y;

    public static Point operator +(Point p1, Point p2)
    {
        return new Point(p1.X + p2.X, p1.Y + p2.Y);
    }

    public static Point operator -(Point p1, Point p2)
    {
        return new Point(p1.X - p2.X, p1.Y - p2.Y);
    }

    public static Point operator *(Point point, double factor)
    {
        return new Point(point.X * factor, point.Y * factor);
    }

    public static Point operator /(Point point, double factor)
    {
        return new Point(point.X / factor, point.Y / factor);
    }

    public static Point Lerp(Point a, Point b, double t)
    {
        return a * (1 - t) + b * t;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一次方程, 二次方程, 三次方程的求解逻辑:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;internal static class MathUtilities
{
    /// &amp;lt;summary&amp;gt;
    /// 解一元一次方程 (标准形式)
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;a&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;b&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public static IEnumerable&amp;lt;double&amp;gt; SolveLinearFunction(double a, double b)
    {
        yield return -b / a;
    }

    /// &amp;lt;summary&amp;gt;
    /// 解一元二次方程 (标准形式)
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;a&quot;&amp;gt;二次项系数&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;b&quot;&amp;gt;一次项系数&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;c&quot;&amp;gt;常数项&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public static IEnumerable&amp;lt;double&amp;gt; SolveQuadraticFunction(double a, double b, double c)
    {
        if (a == 0)
        {
            foreach (var root in SolveLinearFunction(b, c))
            {
                yield return root;
            }

            yield break;
        }

        var delta = b * b - 4 * a * c;
        if (delta &amp;lt; 0)
        {
            // 没有实数根
            yield break;
        }

        var x1 = (-b + Math.Sqrt(delta)) / (2 * a);
        yield return x1;

        if (delta == 0)
        {
            yield break;
        }

        var x2 = (-b - Math.Sqrt(delta)) / (2 * a);
        yield return x2;
    }

    /// &amp;lt;summary&amp;gt;
    /// 解一元三次方程 (标准形式)
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;a&quot;&amp;gt;三次项系数&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;b&quot;&amp;gt;二次项系数&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;c&quot;&amp;gt;一次项系数&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;d&quot;&amp;gt;常数项&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public static IEnumerable&amp;lt;double&amp;gt; SolveCubicFunction(double a, double b, double c, double d)
    {
        if (a == 0)
        {
            foreach (var root in SolveQuadraticFunction(b, c, d))
            {
                yield return root;
            }

            yield break;
        }

        // 盛金公式法

        double bigA = b * b - 3 * a * c;
        double bigB = b * c - 9 * a * d;
        double bigC = c * c - 3 * b * d;

        double delta = bigB * bigB - 4 * bigA * bigC;

        if (bigA == bigB &amp;amp;&amp;amp; bigA == 0)
        {
            // 三个相同的实数根
            yield return -c / b;
            yield break;
        }
        else if (delta &amp;gt; 0)
        {
            var bigY1 = bigA * b + 3 * a * ((-bigB + Math.Sqrt(delta)) / 2);
            var bigY2 = bigA * b + 3 * a * ((-bigB - Math.Sqrt(delta)) / 2);

            // 只有一个实数根, 剩下两个是虚的
            yield return (-b - (Math.Pow(bigY1, 1.0 / 3.0) + Math.Pow(bigY2, 1.0 / 3.0))) / (3 * a);
        }
        else if (delta == 0)
        {
            var bigK = bigB / bigA;

            yield return -b / a + bigK;

            // 两个相同的实数根
            yield return -bigK / 2;
        }
        else
        {
            var bigT = (2 * bigA * b - 3 * a * bigB) / (2 * Math.Pow(bigA, 3.0 / 2.0));
            var theta = Math.Acos(bigT);

            // 三个根
            yield return (-b - 2 * Math.Sqrt(bigA) * Math.Cos(theta / 3)) / (3 * a);
            yield return (-b + Math.Sqrt(bigA) * (Math.Cos(theta / 3) + Math.Sqrt(3) * Math.Sin(theta / 3))) / (3 * a);
            yield return (-b + Math.Sqrt(bigA) * (Math.Cos(theta / 3) - Math.Sqrt(3) * Math.Sin(theta / 3))) / (3 * a);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;贝塞尔曲线抽象:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class BezierCurve
{
    private readonly Point[] _points;

    public BezierCurve(IEnumerable&amp;lt;Point&amp;gt; points)
    {
        ArgumentNullException.ThrowIfNull(points);

        _points = points.ToArray();

        if (_points.Length &amp;lt; 1)
        {
            throw new ArgumentException(&quot;Point count can not be less than 1&quot;);
        }
    }

    private double SolveTimeForPoint(double controlPoint1, double controlPoint2, double point)
    {
        double a = -controlPoint1 + controlPoint2;
        double b = controlPoint1  - point;


        foreach (var root in MathUtilities.SolveLinearFunction(a, b))
        {
            if (root &amp;lt; 0 || root &amp;gt; 1)
            {
                continue;
            }

            return root;
        }

        return double.NaN;
    }

    private double SolveTimeForPoint(double controlPoint1, double controlPoint2, double controlPoint3, double point)
    {
        double a = controlPoint1 - 2 * controlPoint2 + controlPoint3;
        double b = -2 * controlPoint1 + 2 * controlPoint2;
        double c = controlPoint1 - point;

        foreach (var root in MathUtilities.SolveQuadraticFunction(a, b, c))
        {
            if (root &amp;lt; 0 || root &amp;gt; 1)
            {
                continue;
            }

            return root;
        }

        return double.NaN;
    }

    private double SolveTimeForPoint(double controlPoint1, double controlPoint2, double controlPoint3, double controlPoint4, double point)
    {
        double a = -controlPoint1 + 3 * controlPoint2 - 3 * controlPoint3 + controlPoint4;
        double b = 3 * controlPoint1 - 6 * controlPoint2 + 3 * controlPoint3;
        double c = -3 * controlPoint1 + 3 * controlPoint2;
        double d = controlPoint1 - point;

        foreach (var root in MathUtilities.SolveCubicFunction(a, b, c, d))
        {
            if (root &amp;lt; 0 || root &amp;gt; 1)
            {
                continue;
            }

            return root;
        }

        return double.NaN;
    }

    /// &amp;lt;summary&amp;gt;
    /// 传入进度 t 值, 求曲线上点坐标
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;t&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public Point Solve(double t)
    {
        Point[] buffer = new Point[_points.Length];
        _points.CopyTo(buffer, 0);

        int pointCount = _points.Length;
        while (pointCount &amp;gt; 1)
        {
            for (int i = 0; i &amp;lt; pointCount - 1; i++)
            {
                buffer[i] = Point.Lerp(buffer[i], buffer[i + 1], t);
            }

            pointCount--;
        }

        return buffer[0];
    }

    /// &amp;lt;summary&amp;gt;
    /// 根据曲线上某点的 X 值求解 t 参数
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;pointX&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    /// &amp;lt;exception cref=&quot;NotSupportedException&quot;&amp;gt;&amp;lt;/exception&amp;gt;
    public double SolveTimeForPointX(double pointX)
    {
        if (_points.Length == 2)
        {
            return SolveTimeForPoint(_points[0].X, _points[1].X, pointX);
        }
        else if (_points.Length == 3)
        {
            return SolveTimeForPoint(_points[0].X, _points[1].X, _points[2].X, pointX);
        }
        else if (_points.Length == 4)
        {
            return SolveTimeForPoint(_points[0].X, _points[1].X, _points[2].X, _points[3].X, pointX);
        }
        else
        {
            throw new NotSupportedException();
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 根据曲线上某点的 Y 值求解 t 参数
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;pointY&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    /// &amp;lt;exception cref=&quot;NotSupportedException&quot;&amp;gt;&amp;lt;/exception&amp;gt;
    public double SolveTimeForPointY(double pointY)
    {
        if (_points.Length == 2)
        {
            return SolveTimeForPoint(_points[0].Y, _points[1].Y, pointY);
        }
        else if (_points.Length == 3)
        {
            return SolveTimeForPoint(_points[0].Y, _points[1].Y, _points[2].Y, pointY);
        }
        else if (_points.Length == 4)
        {
            return SolveTimeForPoint(_points[0].Y, _points[1].Y, _points[2].Y, _points[3].Y, pointY);
        }
        else
        {
            throw new NotSupportedException();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>在 WPF 中使用 ASP.NET Core 处理 HTTP 请求</title><link>https://slimenull.com/posts/202410030950/</link><guid isPermaLink="true">https://slimenull.com/posts/202410030950/</guid><description>直接在 csproj 中添加 Microsoft.AspNetCore.App 的框架引用即可</description><pubDate>Thu, 03 Oct 2024 03:50:00 GMT</pubDate><content:encoded>&lt;p&gt;对于 SDK 样式 csproj 的项目, 只需要添加 Microsoft.AspNetCore.App 的框架引用即可直接使用 ASP.NET Core 的所有 API&lt;/p&gt;
&lt;h2&gt;创建项目&lt;/h2&gt;
&lt;p&gt;首先, 创建一个 SDK 样式 csproj 的 WPF 项目, 并打开项目文件, 它的内容大概是这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&amp;gt;

  &amp;lt;PropertyGroup&amp;gt;
    &amp;lt;OutputType&amp;gt;WinExe&amp;lt;/OutputType&amp;gt;
    &amp;lt;TargetFramework&amp;gt;net8.0-windows&amp;lt;/TargetFramework&amp;gt;
    &amp;lt;Nullable&amp;gt;enable&amp;lt;/Nullable&amp;gt;
    &amp;lt;ImplicitUsings&amp;gt;enable&amp;lt;/ImplicitUsings&amp;gt;
    &amp;lt;UseWPF&amp;gt;true&amp;lt;/UseWPF&amp;gt;
  &amp;lt;/PropertyGroup&amp;gt;

&amp;lt;/Project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后, 在 &lt;code&gt;Project&lt;/code&gt; 节点下添加 &lt;code&gt;ItemGroup&lt;/code&gt; 节点, 并继续向下添加 &lt;code&gt;FrameworkReference&lt;/code&gt; 节点, 然后设置其 &lt;code&gt;Include&lt;/code&gt; 特性值为 &lt;code&gt;Microsoft.AspNetCore.App&lt;/code&gt;. 添加后项目文件大致如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&amp;gt;

  &amp;lt;PropertyGroup&amp;gt;
    &amp;lt;OutputType&amp;gt;WinExe&amp;lt;/OutputType&amp;gt;
    &amp;lt;TargetFramework&amp;gt;net8.0-windows&amp;lt;/TargetFramework&amp;gt;
    &amp;lt;Nullable&amp;gt;enable&amp;lt;/Nullable&amp;gt;
    &amp;lt;ImplicitUsings&amp;gt;enable&amp;lt;/ImplicitUsings&amp;gt;
    &amp;lt;UseWPF&amp;gt;true&amp;lt;/UseWPF&amp;gt;
  &amp;lt;/PropertyGroup&amp;gt;
  
  &amp;lt;ItemGroup&amp;gt;
    &amp;lt;FrameworkReference Include=&quot;Microsoft.AspNetCore.App&quot; /&amp;gt;
  &amp;lt;/ItemGroup&amp;gt;

&amp;lt;/Project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;为了调试时方便, 你也可以将 &lt;code&gt;PropertyGroup&lt;/code&gt; 下的 &lt;code&gt;OutputType&lt;/code&gt; 设置为 &lt;code&gt;Exe&lt;/code&gt;, 这样程序在启动的时候就会带有控制台.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;编写主逻辑&lt;/h2&gt;
&lt;p&gt;现在, 我们就可以像一个正常的 ASP.NET Core 项目一样, 使用 &lt;code&gt;WebApplication&lt;/code&gt; 和 &lt;code&gt;WebApplicationBuilder&lt;/code&gt; 进行后端逻辑的编写了.&lt;/p&gt;
&lt;p&gt;接下来, 转到 &lt;code&gt;App.xaml.cs&lt;/code&gt;, 重写 &lt;code&gt;OnStartup&lt;/code&gt; 方法, 并添加自己的 Web 初始化和运行逻辑, 最后结果大致如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Interaction logic for App.xaml
/// &amp;lt;/summary&amp;gt;
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        var builder = WebApplication.CreateBuilder();

        builder.Services.AddControllers();                  // 添加控制器
        builder.WebHost.UseUrls(&quot;http://localhost:5000&quot;);   // 更改监听地址

        var app = builder.Build();

        app.UseDefaultFiles();
        app.UseStaticFiles();
        app.MapControllers();

        app.RunAsync();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来, 我们在项目中添加一个 &lt;code&gt;TestController&lt;/code&gt; 类, 用于测试后端请求处理:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Route(&quot;[controller]&quot;)]
public class TestController : Controller
{
    [HttpGet]
    public object Get()
    {
        return new
        {
            Fuck = &quot;You world&quot;
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最终效果&lt;/h2&gt;
&lt;p&gt;启动程序, 并在浏览器访问 &lt;code&gt;http://localhost:5000/test&lt;/code&gt;, 可以看到结果:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/UseAspNetCoreInWpf_BrowserPreview.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;关于此主题的更完整示例, 可以在 GitHub 上 &lt;a href=&quot;https://github.com/SlimeNull/UseAspNetCoreInWpf&quot;&gt;SlimeNull/UseAspNetCoreInWpf&lt;/a&gt; 仓库中查看.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Snipaste_2024-10-03_04-01-40.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>透视转换, 从一个四边形, 映射到另一个四边形</title><link>https://slimenull.com/posts/202410030308/</link><guid isPermaLink="true">https://slimenull.com/posts/202410030308/</guid><pubDate>Thu, 03 Oct 2024 03:08:00 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;透视变换用于将在坐标系中的一个四边形映射到另一个四边形. 相较于仿射变换, 透视变换前平行的线, 并不保证再变换后仍然平行.
但直线在变换之后仍然是直线.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Snipaste_2024-10-03_03-11-59.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一般的, 是通过两四边形的点, 获取一个表示透视变换的矩阵, 对点做乘法, 最后对结果做处理, 即可得到变换之后的点.&lt;/p&gt;
&lt;p&gt;$
M \times P = R
$&lt;/p&gt;
&lt;p&gt;不过这里的点, 需要作为三行一列的矩阵传入, 第三行传入 1:&lt;/p&gt;
&lt;p&gt;$
\left[
\begin{matrix}
? &amp;amp; ? &amp;amp; ? \
? &amp;amp; ? &amp;amp; ? \
? &amp;amp; ? &amp;amp; ? \
\end{matrix}
\right] \times
\left[
\begin{matrix}
x \
y \
1 \
\end{matrix}
\right] =
\left[
\begin{matrix}
x^{\prime} \
y^{\prime} \
w^{\prime} \
\end{matrix}
\right]
$&lt;/p&gt;
&lt;p&gt;最后变换的结果就是 $(x^{\prime} \div w^{\prime}, y^{\prime} \div w^{\prime})$.&lt;/p&gt;
&lt;h2&gt;实操&lt;/h2&gt;
&lt;p&gt;使用 OpenCV, 即可轻松实现. &lt;code&gt;Cv2.GetPerspectiveTransform&lt;/code&gt; 传入两个表示四边形点的可迭代对象, 返回一个三行三列的透视变换矩阵:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Mat GetPerspectiveTransform(IEnumerable&amp;lt;Point2f&amp;gt; src, IEnumerable&amp;lt;Point2f&amp;gt; dst);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么, 假设我们有两个长度为 4 的 Point2f 数组, 每一个数组存储四边形的四个点坐标, 那么通过以下操作, 即可得到透视变换矩阵:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Point2f[] srcQard;   // 源四边形
Point2f[] destQard;  // 目标四边形

Mat perspectiveTransform = Cv2.GetPerspectiveTransform(srcQard, destQard);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来, 假设我们有要进行变换的点的 x y 坐标, 然后构造一个存储该点的矩阵:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;double x;  // X
double y;  // Y

Mat matrixPoint = new Mat&amp;lt;double&amp;gt;(3, 1);
matrixPoint.Set&amp;lt;double&amp;gt;(0, 0, x);
matrixPoint.Set&amp;lt;double&amp;gt;(1, 0, y);
matrixPoint.Set&amp;lt;double&amp;gt;(2, 0, 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进行乘法运算, 并取得变换结果:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Mat result = perspectiveTransform * matrixPoint;

double resultX = result.Get&amp;lt;double&amp;gt;(0, 0) / result.Get&amp;lt;double&amp;gt;(2, 0);
double resultY = result.Get&amp;lt;double&amp;gt;(1, 0) / result.Get&amp;lt;double&amp;gt;(2, 0);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C#] 任意频率 PCM 正弦波生成与 WAV 格式文件保存</title><link>https://slimenull.com/posts/202408191228/</link><guid isPermaLink="true">https://slimenull.com/posts/202408191228/</guid><pubDate>Mon, 19 Aug 2024 12:27:26 GMT</pubDate><content:encoded>&lt;p&gt;封装一个简单的 &lt;code&gt;PcmBuilder&lt;/code&gt;, 以 &lt;code&gt;IEnumerable&amp;lt;float&amp;gt;&lt;/code&gt; 的形式提供采样值:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class PcmBuilder
{
    private PcmBuilder() { }

    public float SampleRate { get; set; } = 44100;
    public float Frequency { get; set; } = 4000;
    public float Amplitude { get; set; } = 1;

    public PcmBuilder WithSampleRate(long sampleRate)
    {
        SampleRate = sampleRate;
        return this;
    }

    public PcmBuilder WithFrequency(float frequency)
    {
        Frequency = frequency;
        return this;
    }

    public PcmBuilder WithAmplitude(float amplitude)
    {
        Amplitude = amplitude;
        return this;
    }

    public IEnumerable&amp;lt;float&amp;gt; Build()
    {
        float current = 0;
        while (true)
        {
            yield return MathF.Sin(current / SampleRate * Frequency) * Amplitude;

            current++;
        }
    }

    public static PcmBuilder CreateNew() =&amp;gt; new PcmBuilder();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用它构建出 44100Hz 采样率, 8000Hz 的正弦波:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var pcm = PcmBuilder.CreateNew()
    .WithSampleRate(44100)
    .WithFrequency(8000)
    .Build();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WAV 文件的头格式:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/the_canonical_wave_file_format.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;WAV 文件的头结构体定义:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public unsafe struct WaveHeader
{
    private fixed byte _chunkId[4];
    private uint _chunkSize;
    private fixed byte _format[4];

    private fixed byte _subChunk1Id[4];
    private uint _subChunk1Size;
    private WaveAudioFormat _audioFormat;
    private ushort _numChannels;
    private uint _sampleRate;
    private uint _byteRate;
    private ushort _blockAlign;
    private ushort _bitsPerSample;

    private fixed byte _subChunk2Id[4];
    private uint _subChunk2Size;

    public unsafe string ChunkId
    {
        get
        {
            fixed (byte* ptr = _chunkId)
            {
                return CreateString(ptr, 4);
            }
        }
        set
        {
            fixed (byte* ptr = _chunkId)
            {
                FillString(ptr, value, 4);
            }
        }
    }

    public uint ChunkSize
    {
        get =&amp;gt; _chunkSize;
        set =&amp;gt; _chunkSize = value;
    }

    public string Format
    {
        get
        {
            fixed (byte* ptr = _format)
            {
                return CreateString(ptr, 4);
            }
        }
        set
        {
            fixed (byte* ptr = _format)
            {
                FillString(ptr, value, 4);
            }
        }
    }

    public string SubChunk1Id
    {
        get
        {
            fixed (byte* ptr = _subChunk1Id)
            {
                return CreateString(ptr, 4);
            }
        }
        set
        {
            fixed (byte* ptr = _subChunk1Id)
            {
                FillString(ptr, value, 4);
            }
        }
    }

    public uint SubChunk1Size
    {
        get =&amp;gt; _subChunk1Size;
        set =&amp;gt; _subChunk1Size = value;
    }

    public WaveAudioFormat AudioFormat
    {
        get =&amp;gt; _audioFormat;
        set =&amp;gt; _audioFormat = value;
    }

    public ushort ChannelCount
    {
        get =&amp;gt; _numChannels;
        set =&amp;gt; _numChannels = value;
    }

    public uint SampleRate
    {
        get =&amp;gt; _sampleRate;
        set =&amp;gt; _sampleRate = value;
    }

    public uint ByteRate
    {
        get =&amp;gt; _byteRate;
        set =&amp;gt; _byteRate = value;
    }

    public ushort BlockAlign
    {
        get =&amp;gt; _blockAlign;
        set =&amp;gt; _blockAlign = value;
    }

    public ushort BitsPerSample
    {
        get =&amp;gt; _bitsPerSample;
        set =&amp;gt; _bitsPerSample = value;
    }

    public string SubChunk2Id
    {
        get
        {
            fixed (byte* ptr = _subChunk2Id)
            {
                return CreateString(ptr, 4);
            }
        }
        set
        {
            fixed (byte* ptr = _subChunk2Id)
            {
                FillString(ptr, value, 4);
            }
        }
    }

    public uint SubChunk2Size
    {
        get =&amp;gt; _subChunk2Size;
        set =&amp;gt; _subChunk2Size = value;
    }

    public unsafe static WaveHeader Create(WaveAudioFormat audioFormat, ushort channelCount, uint sampleRate, ushort bitsPerSample, uint pcmDataSize)
    {
        return new WaveHeader()
        {
            ChunkId = &quot;RIFF&quot;,
            ChunkSize = (uint)(pcmDataSize + (sizeof(WaveHeader) - 8)),
            Format = &quot;WAVE&quot;,

            SubChunk1Id = &quot;fmt &quot;,
            SubChunk1Size = 16,
            AudioFormat = audioFormat,
            ChannelCount = channelCount,
            SampleRate = sampleRate,
            ByteRate = sampleRate * channelCount * bitsPerSample / 8,
            BlockAlign = (ushort)(channelCount * bitsPerSample / 8),
            BitsPerSample = bitsPerSample,

            SubChunk2Id = &quot;data&quot;,
            SubChunk2Size = pcmDataSize
        };
    }

    private static void FillString(byte* ptr, string value, int maxLength)
    {
        if (value.Length &amp;gt; maxLength)
        {
            throw new ArgumentException(nameof(value));
        }

        fixed (char* textPtr = value)
        {
            for (int i = 0; i &amp;lt; value.Length &amp;amp;&amp;amp; i &amp;lt; maxLength; i++)
            {
                ptr[i] = (byte)textPtr[i];
            }
        }
    }

    private static string CreateString(byte* ptr, int maxLength)
    {
        StringBuilder sb = new StringBuilder(maxLength);
        for (int i = 0; i &amp;lt; maxLength; i++)
        {
            if (ptr[i] == 0)
            {
                break;
            }

            sb.Append((char)ptr[i]);
        }

        return sb.ToString();
    }
}

public enum WaveAudioFormat : ushort
{
    None = 0,
    PCM = 1,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用它构建 PCM 格式, 单声道, 44100Hz 采样率, 32位深, 30 秒的音频 WAV 头:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var header = WaveHeader.Create(WaveAudioFormat.PCM, 1, 44100, 32, 44100 * 4 * 30);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建文件, 并写入 WAV 头和 PCM 内容:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using var output = File.Create(&quot;output.wav&quot;);
using var outputWriter = new BinaryWriter(output);

var pcm = PcmBuilder.CreateNew()
    .WithSampleRate(44100)
    .WithFrequency(8000)
    .Build();

var header = WaveHeader.Create(WaveAudioFormat.PCM, 1, 44100, 32, 44100 * 4 * 30);

// 写入 Header
unsafe
{
    var headerByteSpan = new Span&amp;lt;byte&amp;gt;((byte*)&amp;amp;header, sizeof(WaveHeader));
    outputWriter.Write(headerByteSpan);
}

// 获取 PCM 数据迭代器
var pcmEnumerator = pcm.GetEnumerator();

// 向文件写入 PCM
for (int i = 0; i &amp;lt; 44100 * 30; i++)
{
    pcmEnumerator.MoveNext();

    outputWriter.Write(pcmEnumerator.Current);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>关于各音频编码格式的数据波动幅度与音量的对照关系</title><link>https://slimenull.com/posts/20240605201013/</link><guid isPermaLink="true">https://slimenull.com/posts/20240605201013/</guid><pubDate>Wed, 05 Jun 2024 20:10:13 GMT</pubDate><content:encoded>&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;采样格式&lt;/th&gt;
&lt;th&gt;到达满音量所需要的数据波动幅度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;32位浮点数&lt;/td&gt;
&lt;td&gt;-1 到 1, 也就是 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;带符号 16 位整数&lt;/td&gt;
&lt;td&gt;-32768 到 32767, 也就是 65535&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其他位深的整数格式存档, 其满音量所需数据波动幅度也均为该格式的最小值到最大值&lt;/p&gt;
</content:encoded></item><item><title>[.NET,WPF] 窗体云母, 亚克力, 透明, 混合颜色, 模糊背景, 亮暗色主题全讲</title><link>https://slimenull.com/posts/20240530104846/</link><guid isPermaLink="true">https://slimenull.com/posts/20240530104846/</guid><description>在 WPF 中正确的使用云母, 亚克力等透明背景材质, 以及进行亮暗主题切换</description><pubDate>Thu, 30 May 2024 10:48:46 GMT</pubDate><content:encoded>&lt;p&gt;众所周知, 在 Windows10 中亚克力(Acrylic)效果被添加, 在 Windows11 中云母(Mica)效果被添加. 其中亚克力效果简单来讲就是对窗体后的元素模糊再加噪点. Mica 则是对壁纸进行模糊再加噪点.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;实现&lt;/th&gt;
&lt;th&gt;效果&lt;/th&gt;
&lt;th&gt;性能&lt;/th&gt;
&lt;th&gt;推荐用于&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;毛玻璃/Aero&lt;/td&gt;
&lt;td&gt;透明+模糊&lt;/td&gt;
&lt;td&gt;好&lt;/td&gt;
&lt;td&gt;最差&lt;/td&gt;
&lt;td&gt;我不知道新版本操作系统怎么启用 Aero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;亚克力/Acrylic&lt;/td&gt;
&lt;td&gt;透明+模糊+噪点&lt;/td&gt;
&lt;td&gt;好&lt;/td&gt;
&lt;td&gt;较差&lt;/td&gt;
&lt;td&gt;临时窗口, 例如右键菜单, 工具提示等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;云母/Mica&lt;/td&gt;
&lt;td&gt;壁纸采样+模糊+噪点&lt;/td&gt;
&lt;td&gt;较好&lt;/td&gt;
&lt;td&gt;最好&lt;/td&gt;
&lt;td&gt;主窗口&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;云母效果的性能之所以好, 是因为它不会对壁纸进行重复采样, 而是只采样一次, 生成对应云母效果贴图, 最后只需要将它应用到窗口上就可以. 可以说, 基本上没有任何消耗.&lt;/p&gt;
&lt;p&gt;但这也带来了一些弊端, 假如你使用了某种动态壁纸软件, 那么云母效果和你的壁纸是完全不匹配的, 这稍稍影响了美观.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在阅读本文章之前, 请确保你有进行 .NET 调用非托管动态链接库函数的经验, 大致知道 &lt;code&gt;WindowChrome&lt;/code&gt; 类的作用, 知道窗口句柄是什么东西.
阅读时请从开始到结束按顺序看, 否则可能会看漏某些细节上的东西.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h2&gt;背景模糊 API&lt;/h2&gt;
&lt;p&gt;在 Windows 中, 有两个 API 可以用来实现背景模糊:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;DwmSetWindowAttribute&lt;/th&gt;
&lt;th&gt;SetWindowCompositionAttribute&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;可以实现的功能&lt;/td&gt;
&lt;td&gt;亮暗色模式的云母与亚克力效果&lt;/td&gt;
&lt;td&gt;支持自定义混合颜色的亚克力效果, 模糊透明效果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模糊区域&lt;/td&gt;
&lt;td&gt;能够应用到整个窗口, 包括标题栏以及窗口内容&lt;/td&gt;
&lt;td&gt;只能对窗口内容进行模糊&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;系统要求&lt;/td&gt;
&lt;td&gt;API 需要 Windows Vista 及以上版本, 亮暗色主题, 云母和亚克力效果则只能在 Windows11 上用, 其中亮暗色需要版本 22000, 模糊背景需要版本 22621&lt;/td&gt;
&lt;td&gt;API 在 Windows 7 及以上版本可用, 亚克力与模糊背景效果则只能在 Windows10 上可用, 具体版本未知, 因为这是未在微软文档中具体写明的 API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;其他要求&lt;/td&gt;
&lt;td&gt;只有在 &quot;窗口框架&quot; 拓展到窗口工作区内时, 拓展的厚度才会带有对应模糊效果.&lt;/td&gt;
&lt;td&gt;只有在 &quot;窗口框架&quot; 不拓展到窗口工作区内时有效.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;优点&lt;/td&gt;
&lt;td&gt;通过设置颜色主题即可直接自动换掉云母或亚克力背景的颜色&lt;/td&gt;
&lt;td&gt;在窗口失去焦点时, 模糊仍然有效, 并且可以自定义混合颜色&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缺点&lt;/td&gt;
&lt;td&gt;当窗体失去焦点的时候, 模糊效果会消失&lt;/td&gt;
&lt;td&gt;没有云母效果&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;两 API 在 C# 的声明分别如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[DllImport(&quot;DWMAPI&quot;)]
public static extern nint DwmSetWindowAttribute(nint hwnd, DwmWindowAttribute attribute, nint dataPointer, uint dataSize);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[DllImport(&quot;User32&quot;)]
public static extern bool SetWindowCompositionAttribute(nint hwnd, ref WindowCompositionAttributeData data);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它们依赖的, 用于设置 &quot;窗口框架&quot; 拓展厚度的 API 声明如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[DllImport(&quot;DWMAPI&quot;)]
public static extern nint DwmExtendFrameIntoClientArea(nint hwnd, ref Margins margins);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结构体与枚举声明如下:&lt;/p&gt;
&lt;p&gt;DWM 属性枚举&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public enum DwmWindowAttribute
{
    NCRENDERING_ENABLED,
    NCRENDERING_POLICY,
    TRANSITIONS_FORCEDISABLED,
    ALLOW_NCPAINT,
    CAPTION_BUTTON_BOUNDS,
    NONCLIENT_RTL_LAYOUT,
    FORCE_ICONIC_REPRESENTATION,
    FLIP3D_POLICY,
    EXTENDED_FRAME_BOUNDS,
    HAS_ICONIC_BITMAP,
    DISALLOW_PEEK,
    EXCLUDED_FROM_PEEK,
    CLOAK,
    CLOAKED,
    FREEZE_REPRESENTATION,
    PASSIVE_UPDATE_MODE,
    USE_HOSTBACKDROPBRUSH,

    // 表示是否使用暗色模式, 它会将窗体的模糊背景调整为暗色
    USE_IMMERSIVE_DARK_MODE = 20,
    WINDOW_CORNER_PREFERENCE = 33,
    BORDER_COLOR,
    CAPTION_COLOR,
    TEXT_COLOR,
    VISIBLE_FRAME_BORDER_THICKNESS,

    // 背景类型, 值可以是: 自动, 无, 云母, 或者亚克力
    SYSTEMBACKDROP_TYPE,
    LAST
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;背景类型枚举:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public enum WindowBackdrop
{
    Auto = 0,
    None = 1,
    MainWindow = 2,
    TransientWindow = 3,
    TabbedWindow = 4
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置 &lt;code&gt;SetWindowCompositionAttribute&lt;/code&gt; 所用的数据结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[StructLayout(LayoutKind.Sequential)]
public struct WindowCompositionAttributeData
{
    /// &amp;lt;summary&amp;gt;
    /// A flag describing which value to get or set, specified as a value of the &amp;lt;see cref=&quot;WindowCompositionAttribute&quot;/&amp;gt; enumeration. 
    /// This parameter specifies which attribute to get or set, and the pvData member points to an object containing the attribute value.
    /// &amp;lt;/summary&amp;gt;
    public WindowCompositionAttribute Attribute;

    /// &amp;lt;summary&amp;gt;
    /// When used with the GetWindowCompositionAttribute function, this member contains a pointer to a variable that will hold the value of the requested attribute when the function returns. &amp;lt;br/&amp;gt;
    /// When used with the SetWindowCompositionAttribute function, it points an object containing the attribute value to set. &amp;lt;br/&amp;gt;
    /// The type of the value set depends on the value of the Attrib member.
    /// &amp;lt;/summary&amp;gt;
    public nint DataPointer;

    /// &amp;lt;summary&amp;gt;
    /// The size of the object pointed to by the pvData member, in bytes.
    /// &amp;lt;/summary&amp;gt;
    public uint DataSize;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;SetWindowCompositionAttribute&lt;/code&gt; 设置背景模糊时所需要用到的属性枚举&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public enum WindowCompositionAttribute
{
    // 省略其他未使用的字段
    WcaAccentPolicy = 19,
    // 省略其他未使用的字段
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;SetWindowCompositionAttribute&lt;/code&gt;  设置背景模糊需要实际传入的数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[StructLayout(LayoutKind.Sequential)]
public struct AccentPolicy
{
    public AccentState AccentState;
    public AccentFlags AccentFlags;
    public int GradientColor;
    public int AnimationId;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Accent state, 即在窗口上要使用到的效果:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public enum AccentState
{
    Disabled,
    EnableGradient = 1,           // 渐变 (实测没什么用)
    EnableTransparent = 2,        // 透明 (实测没什么用)
    EnableBlurBehind = 3,         // 背景模糊 (有用)
    EnableAcrylicBlurBehind = 4,  // 背景亚克力模糊 (有用)
    EnableHostBackdrop = 5,       // 没啥用
    InvalidState = 6,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Accent flags, 控制某些行为, 由于没有文档, 下面的值都是测试出来的:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Flags]
public enum AccentFlags
{
    None = 0,
    ExtendSize = 0x4,      // 启用此 flag 会导致窗体大小拓展至屏幕大小
    LeftBorder = 0x20,     // 启用窗口左侧边框 (当 WindowStyle 为 None 时可以看出来)
    TopBorder = 0x40,      // 启用窗口顶部边框 (同上)
    RightBorder = 0x80,    // 启用窗口右侧边框 (同上)
    BottomBorder = 0x100,  // 启用窗口底部边框

    // 合起来, 启用窗口所有边框
    AllBorder = LeftBorder | TopBorder | RightBorder | BottomBorder,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置 &quot;窗口拓展&quot; 需要用到的表示拓展大小的结构. 当值为 -1 时表示无穷大.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[StructLayout(LayoutKind.Sequential)]
public struct Margins
{
    public int LeftWidth;
    public int RightWidth;
    public int TopHeight;
    public int BottomHeight;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;应用云母材质&lt;/h2&gt;
&lt;p&gt;要对 WPF 的窗口进行操作, 我们需要拿到窗口的句柄, 然后调用刚刚提到的 API 进行操作.&lt;/p&gt;
&lt;p&gt;不过在这之前, 我们还需要知道一件事. WPF 要应用背景透明模糊效果, 还需要设置窗口对应 &lt;code&gt;HwndSource&lt;/code&gt; 的 &lt;code&gt;CompositionTarget&lt;/code&gt; 的 &lt;code&gt;BackgroundColor&lt;/code&gt; 为 &lt;code&gt;Transparent&lt;/code&gt; 才能正确的看到透明背景. 除此之外, 你也需要确认窗口的 &lt;code&gt;Background&lt;/code&gt; 也是 &lt;code&gt;Transparent&lt;/code&gt;, 否则窗口颜色遮挡背景, 你就看不到了.&lt;/p&gt;
&lt;p&gt;由于在 WPF 程序中, 一切都抽象成了 WPF 的对象, 而对于窗口这一类型来讲, 在它被调用 Show 方法之前, 是不会创建 Win32 窗口的. 要保证我们能够持有窗口句柄, 有两种方案:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;WindowsInteropHelper&lt;/code&gt; 的 &lt;code&gt;EnsureHandle&lt;/code&gt; 方法, 确保窗口对象创建了 Win32 窗口并可以使用句柄.&lt;/li&gt;
&lt;li&gt;订阅窗口的 &lt;code&gt;SourceInitialized&lt;/code&gt; 事件, 该事件会在窗口的 Win32 窗口被创建之后引发.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面我们以 &lt;code&gt;SourceInitialized&lt;/code&gt; 事件的事件处理器中编写逻辑举例, 讲述如何正确的在 WPF 中应用云母效果.&lt;/p&gt;
&lt;p&gt;首先, 确保我们的窗口背景颜色为 &lt;code&gt;Transparent&lt;/code&gt;, 你的 XAML 代码应该像这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Window ...
        Background=&quot;Transparent&quot;&amp;gt;
    ...
&amp;lt;/Window&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你的设置无误, 那么你的窗口现在的 &quot;透明&quot; 看起来应该是 &quot;黑色&quot;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240530134522.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后, 实现 &lt;code&gt;HwndSource&lt;/code&gt; 的 &lt;code&gt;CompositionTarget&lt;/code&gt; 设置和 &quot;窗口框架&quot;, 最简单的方式是直接为窗口设置 &lt;code&gt;WindowChrome&lt;/code&gt;, 通过对 &lt;code&gt;WindowChrome&lt;/code&gt; 类中名为 &lt;code&gt;WindowChrome&lt;/code&gt; 的附加属性进行赋值即可, 最后只需要设置 &lt;code&gt;GlassFrameThickness&lt;/code&gt; 为 -1 就完成了, 在 XAML 中这样编写:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Window ...
        Background=&quot;Transparent&quot;&amp;gt;
    &amp;lt;WindowChrome.WindowChrome&amp;gt;
        &amp;lt;WindowChrome GlassFrameThickness=&quot;-1&quot;/&amp;gt;
    &amp;lt;/WindowChrome.WindowChrome&amp;gt;
    ...
&amp;lt;/Window&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;窗口边框拓展大小&lt;/code&gt; 控制了窗口设置的模糊背景延伸到窗口内部的大小. 也就是说, 如果你将 &lt;code&gt;GlassFrameThickness&lt;/code&gt; 设置为 10, 那么窗口边缘的 10 像素会应用上指定的背景. 而 -1 在这里则表示无限大.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这样操作之后, &lt;code&gt;WindowChrome&lt;/code&gt; 会帮我们设置好所需的属性, 如果你的设置无误, 现在窗口应该会恢复到原来的 &quot;白色&quot;, 并且窗口的标题栏会 &quot;消失&quot;, 只留下三个窗口按钮:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240530134637.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后, 我们订阅窗口的 &lt;code&gt;SourceInitialized&lt;/code&gt; 事件:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Window ...
        Background=&quot;Transparent&quot;
        SourceInitialized=&quot;Window_SourceInitialized&quot;&amp;gt;
    &amp;lt;WindowChrome.WindowChrome&amp;gt;
        &amp;lt;WindowChrome GlassFrameThickness=&quot;-1&quot;/&amp;gt;
    &amp;lt;/WindowChrome.WindowChrome&amp;gt;
    ...
&amp;lt;/Window&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并添加以下处理:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private unsafe void Window_SourceInitialized(object sender, EventArgs e)
{
    var hwndSource = (HwndSource)PresentationSource.FromVisual(this);
    var backdropType = (int)2;   // 0 到 4 分别是 &apos;自动&apos;, &apos;无&apos;, &apos;云母(Mica)效果&apos;, &apos;亚克力(Acrylic)效果&apos;, &apos;云母备选(Mica Alt)效果&apos;

    DwmSetWindowAttribute(hwndSource.Handle, DwmWindowAttribute.SYSTEMBACKDROP_TYPE, (nint)(void*)&amp;amp;backdropType, sizeof(int));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意, 在这里使用到了不安全代码块, 你需要在你的项目文件中启用 &quot;允许不安全代码块&quot; 才能使用此功能.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最后启动窗口, 操作无误的话, 你最终的窗口, 已经能够呈现出云母效果了.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240530135314.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h2&gt;应用亚克力材质&lt;/h2&gt;
&lt;p&gt;在上面, 我们已经成功将云母材质应用到了窗口上, 如果要换成亚克力效果, 也非常简单, 只需要将表示云母背景的 &quot;2&quot; 换成表示亚克力的 &quot;3&quot; 即可, 代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private unsafe void Window_SourceInitialized(object sender, EventArgs e)
{
    var hwndSource = (HwndSource)PresentationSource.FromVisual(this);
    var backdropType = (int)3;   // 0 到 4 分别是 &apos;自动&apos;, &apos;无&apos;, &apos;云母(Mica)效果&apos;, &apos;亚克力(Acrylic)效果&apos;, &apos;云母备选(Mica Alt)效果&apos;

    DwmSetWindowAttribute(hwndSource.Handle, DwmWindowAttribute.SYSTEMBACKDROP_TYPE, (nint)(void*)&amp;amp;backdropType, sizeof(int));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么, 最终的效果是这样, 可以看到窗口的背景是参考了其后方窗口内容的:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240530140028.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h2&gt;切换窗口亮暗色主题&lt;/h2&gt;
&lt;p&gt;在 Windows11 的 22000 版本中, &lt;code&gt;DwmSetWindowAttribute&lt;/code&gt; 支持了表示窗口是否使用暗色模式的 &lt;code&gt;DWMWA_USE_IMMERSIVE_DARK_MODE&lt;/code&gt; 属性.&lt;/p&gt;
&lt;p&gt;当我们使用该 API 设置该属性为 1 的时候, 标题栏, 以及云母与亚克力材料会切换到对应的暗色. 下面, 我们向代码中添加将窗口设置为暗色模式的逻辑:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private unsafe void Window_SourceInitialized(object sender, EventArgs e)
{
    var hwndSource = (HwndSource)PresentationSource.FromVisual(this);
    var backdropType = (int)3;   // 0 到 3 分别是 &apos;自动&apos;, &apos;无&apos;, &apos;云母(Mica)效果&apos;, &apos;亚克力(Acrylic)效果&apos;
    var isDarkMode = (int)1;

    DwmSetWindowAttribute(hwndSource.Handle, DwmWindowAttribute.SYSTEMBACKDROP_TYPE, (nint)(void*)&amp;amp;backdropType, sizeof(int));
    DwmSetWindowAttribute(hwndSource.Handle, DwmWindowAttribute.USE_IMMERSIVE_DARK_MODE, (nint)(void*)&amp;amp;isDarkMode, sizeof(int));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们的亚克力窗口背景看起来比以前暗了许多.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240530141132.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果它的背景更暗一些, 窗口看起来也会更暗:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240530141233.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们再将亚克力材质换成云母, 它的暗色模式看起来是这样的, 因为它不参考窗口后的内容, 只参考壁纸, 所以无论窗口在哪, 它都是这副模样:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240530141400.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在调整窗口内容元素前景与背景色到大致合适的值之后, 它看起来是这样的:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240530141807.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h2&gt;自定义混合颜色亚克力&lt;/h2&gt;
&lt;p&gt;在上面, 我们使用 &lt;code&gt;DwmSetWindowAttribute&lt;/code&gt; 实现了窗口的云母与亚克力背景, 而它们具体的颜色, 则是通过设置窗口是否是暗色实现来切换的. 如果我们需要自定义叠加的混合颜色, 那这个 API 就不能实现了.&lt;/p&gt;
&lt;p&gt;我们需要使用 &lt;code&gt;SetWindowCompositionAttribute&lt;/code&gt; 这个 API 来实现了. 当然, 在最开始我们也了解过这个 API 的弊端了, 所以, 除非你真的有这方面的需求, 否则尽量使用新的 API 比较好.&lt;/p&gt;
&lt;p&gt;同样, 在调用 API 之前, 需要确保我们的窗口设置了正确的属性, 第一个是背景颜色需要设为透明:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Window ...
        Background=&quot;Transparent&quot;&amp;gt;
    ...
&amp;lt;/Window&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后, 需要确保 &quot;窗口框架&quot; 的拓展大小是 0, 也就是说, Window 必须不设置 &lt;code&gt;WindowChrome&lt;/code&gt;, 或者 &lt;code&gt;WindowChrome&lt;/code&gt; 的 &lt;code&gt;GlassFrameThickness&lt;/code&gt; 值需要为 0. 以下两种设置方式均可:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Window ...
        Background=&quot;Transparent&quot;&amp;gt;
    &amp;lt;!-- 这里没有设置窗口的 WindowChrome --&amp;gt;
    ...
&amp;lt;/Window&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者设置 &lt;code&gt;WindowChrome&lt;/code&gt;, 但是 &lt;code&gt;GlassFrameThickness&lt;/code&gt; 为 0:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Window ...
        Background=&quot;Transparent&quot;&amp;gt;
    &amp;lt;WindowChrome.WindowChrome&amp;gt;
        &amp;lt;WindowChrome GlassFrameThickness=&quot;0&quot;/&amp;gt;
    &amp;lt;/WindowChrome.WindowChrome&amp;gt;
    ...
&amp;lt;/Window&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后, 确保 &lt;code&gt;HwndSource.CompositionTarget.BackgroundColor&lt;/code&gt; 为透明, 最暴力的方式是直接将窗口的 &lt;code&gt;AllowsTransparency&lt;/code&gt; 设为 &lt;code&gt;true&lt;/code&gt;. 又因为只有 &lt;code&gt;WindowStyle&lt;/code&gt; 为 &lt;code&gt;None&lt;/code&gt; 是, 窗口才能设置 &lt;code&gt;AllowsTransparency&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;,  所以最终你的 XAML 像是这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Window ...
        Background=&quot;Transparent&quot;
        WindowStyle=&quot;None&quot;
        AllowsTransparency=&quot;True&quot;
        SourceInitialized=&quot;Window_SourceInitialized&quot;&amp;gt;
    &amp;lt;WindowChrome.WindowChrome&amp;gt;
        &amp;lt;WindowChrome GlassFrameThickness=&quot;0&quot;/&amp;gt;
    &amp;lt;/WindowChrome.WindowChrome&amp;gt;
    ...
&amp;lt;/Window&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就此, 我们的准备阶段完毕, 最后将 &lt;code&gt;SourceInitialized&lt;/code&gt; 事件处理逻辑更改为以下代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private unsafe void Window_SourceInitialized(object sender, EventArgs e)
{
    var hwndSource = (HwndSource)PresentationSource.FromVisual(this);

    var accentPolicy = new AccentPolicy()
    {
        AccentState = AccentState.EnableAcrylicBlurBehind,
        AccentFlags = AccentFlags.None,

        // 注意, 这里从高位到低位分别是 Alpha, Blue, Green, Red
        // 每一个字节一个通道颜色值
        GradientColor = 0x338888FF,
    };

    var data = new WindowCompositionAttributeData()
    {
        Attribute = WindowCompositionAttribute.WcaAccentPolicy,
        DataPointer = (nint)(void*)&amp;amp;accentPolicy,
        DataSize = (uint)sizeof(AccentPolicy),
    };

    SetWindowCompositionAttribute(hwndSource.Handle, ref data);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行后, 它的效果是偏红色的亚克力窗口.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240530144501.png&quot; alt=&quot;Pasted image 20240530144501&quot; /&gt;&lt;/p&gt;
&lt;p&gt;相信你也一定发现了一些问题, 经过一番设置之后, 我们的窗口:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于设置了 &lt;code&gt;AllowsTransparency&lt;/code&gt; 为 &lt;code&gt;True&lt;/code&gt;, 窗口中的 &quot;透明&quot; 部分, 鼠标事件会穿透它, 从而点击到窗体后方的内容.&lt;/li&gt;
&lt;li&gt;由于设置了 &lt;code&gt;WindowStyle&lt;/code&gt; 为 &lt;code&gt;None&lt;/code&gt;, 窗口标题栏没有了, 按钮没有了, 圆角没有了, 最小化最大化与关闭的动画没有了.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于第一点, 可以通过一种简单暴力的方式来解决, 只需要给窗口的不透明度一个很小的值, 即可:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Window ...
        Background=&quot;#01000000&quot;
        WindowStyle=&quot;None&quot;
        AllowsTransparency=&quot;True&quot;
        SourceInitialized=&quot;Window_SourceInitialized&quot;&amp;gt;
    &amp;lt;WindowChrome.WindowChrome&amp;gt;
        &amp;lt;WindowChrome GlassFrameThickness=&quot;0&quot;/&amp;gt;
    &amp;lt;/WindowChrome.WindowChrome&amp;gt;
    ...
&amp;lt;/Window&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而第二点, 本质上是因为我们通过设置 &lt;code&gt;WindowStyle&lt;/code&gt; 以满足调用条件导致的, 只需要我们不去设置 &lt;code&gt;WindowStyle&lt;/code&gt;, 从底层上自己满足调用条件, 就可以解决. 具体的操作方式, 请继续往下看.&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h2&gt;从底层上满足调用条件&lt;/h2&gt;
&lt;p&gt;在上面的代码中, 为了满足 &lt;code&gt;DwmSetWindowAttribute&lt;/code&gt; 和 &lt;code&gt;SetWindowCompositionAttribute&lt;/code&gt; 的调用条件, 我们使用了 &lt;code&gt;WindowChrome.WindowChrome&lt;/code&gt;, &lt;code&gt;WindowStyle&lt;/code&gt;, &lt;code&gt;AllowsTransparency&lt;/code&gt; 属性. 而它们也都有自己的副作用:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;WindowChrome&lt;/code&gt; 会去除窗口自带的标题栏, 你还需要手动用 WPF 自己做一个标题栏&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WindowStyle&lt;/code&gt; 设为 &lt;code&gt;None&lt;/code&gt; 会导致窗口的边框消失, 窗口操作按钮消失, 动画消失, 圆角消失.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AllowsTransparency&lt;/code&gt; 设为 &lt;code&gt;True&lt;/code&gt; 会导致透明部分穿透鼠标事件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际上, 最终我们需要满足的条件也只有:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;窗口的背景设为透明, 以能够查看到设置的窗口背景样式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HwndSource.CompositionTarget.BackgroundColor&lt;/code&gt; 设为透明&lt;/li&gt;
&lt;li&gt;当使用 &lt;code&gt;DwmSetWindowAttribute&lt;/code&gt; 时, &quot;窗口框架&quot; 的拓展厚度设置到一个足够大的值, 当使用 &lt;code&gt;SetWindowCompositionAttribute&lt;/code&gt; 时, &quot;窗口框架&quot; 的拓展厚度需要设置为 0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么现在, 我们将多余的属性删去, 仅保留窗体的以下属性:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Window ...
        Background=&quot;Transparent&quot;
        SourceInitialized=&quot;Window_SourceInitialized&quot;&amp;gt;
    ...
&amp;lt;/Window&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 &lt;code&gt;SourceInitialized&lt;/code&gt; 改为以下逻辑, 来实现手动满足条件的云母效果:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private unsafe void Window_SourceInitialized(object sender, EventArgs e)
{
    // 取得窗口句柄
    var hwndSource = (HwndSource)PresentationSource.FromVisual(this);

    // 设置 HwndSource.CompositionTarget.BackgroundColor 为透明
    hwndSource.CompositionTarget.BackgroundColor = Colors.Transparent;

    // 设置边框
    var margins = new Margins()
    {
        LeftWidth = -1,
        TopHeight = -1,
        RightWidth = -1,
        BottomHeight = -1
    };

    DwmExtendFrameIntoClientArea(hwndSource.Handle, ref margins);

    // 设置背景
    var backdrop = (int)2;

    DwmSetWindowAttribute(hwndSource.Handle, DwmWindowAttribute.SYSTEMBACKDROP_TYPE, (nint)(void*)&amp;amp;backdrop, sizeof(int));
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后我们就得到了一个, 带有窗口正常标题栏与云母背景的窗口:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240530152406.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同理, 我们手动处理 &lt;code&gt;SetWindowCompositionAttribute&lt;/code&gt; 的调用条件, XAML 不需要更改, 只需要将逻辑改为下面的代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private unsafe void Window_SourceInitialized(object sender, EventArgs e)
{
    // 取得窗口句柄
    var hwndSource = (HwndSource)PresentationSource.FromVisual(this);

    // 设置 HwndSource.CompositionTarget.BackgroundColor 为透明
    hwndSource.CompositionTarget.BackgroundColor = Colors.Transparent;

    // 设置边框
    var margins = new Margins()
    {
        LeftWidth = 0,
        TopHeight = 0,
        RightWidth = 0,
        BottomHeight = 0
    };

    DwmExtendFrameIntoClientArea(hwndSource.Handle, ref margins);

    // 设置背景
    var accentPolicy = new AccentPolicy()
    {
        AccentState = AccentState.EnableAcrylicBlurBehind,
        AccentFlags = AccentFlags.None,

        // 注意, 这里从高位到低位分别是 Alpha, Blue, Green, Red
        // 每一个字节一个通道颜色值
        GradientColor = 0x338888FF,
    };

    var data = new WindowCompositionAttributeData()
    {
        Attribute = WindowCompositionAttribute.WcaAccentPolicy,
        DataPointer = (nint)(void*)&amp;amp;accentPolicy,
        DataSize = (uint)sizeof(AccentPolicy),
    };

    SetWindowCompositionAttribute(hwndSource.Handle, ref data);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是我们得到了一个带有不透明标题栏与自定义混合颜色亚克力背景的窗口:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240530152815.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h2&gt;错误参考&lt;/h2&gt;
&lt;p&gt;下面是一些属性没有正确设置会导致的结果:&lt;/p&gt;
&lt;p&gt;窗口颜色过曝, 没有将 &lt;code&gt;HwndSource.CompositionTarget.BackgroundColor&lt;/code&gt; 设置为透明导致.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240530153159.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;窗口黑了, &quot;窗口边框&quot; 拓展大小于 API 所需大小不匹配导致.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240530153317.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;可以使用 &lt;code&gt;DwmSetWindowAttribute&lt;/code&gt; 与 &lt;code&gt;SetWindowCompositionAttribute&lt;/code&gt; 设置窗口的背景, 其中前者只能用于 Windows11 上设置亮暗的云母与亚克力材质, 后者能够设置自定义混合颜色的亚克力材质. 前者能够将材质应用到标题栏与内容, 而后者只能将材料应用到内容中.&lt;/p&gt;
&lt;p&gt;这两个 API 均有自己的 &quot;条件&quot;, 只有条件满足, 所指定的背景才能够被用户看到. 否则要么无作用, 要么窗口的内容变得很奇怪.&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;WindowChrome&lt;/code&gt;, &lt;code&gt;WindowStyle&lt;/code&gt; 与 &lt;code&gt;AllowsTransparency&lt;/code&gt; 的根本目的是满足显示指定背景, 我们完全可以自己完整这一操作. 这样就不需要承担那三个属性会导致的副作用.&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h2&gt;引用&lt;/h2&gt;
&lt;p&gt;文章参考:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/TwilightLemon/p/17479921.html&quot;&gt;WPF在win10/11上启用模糊特效 适配Dark/Light Mode - TwilightLemon)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.walterlv.com/post/win10/2017/10/02/wpf-transparent-blur-in-windows-10.html&quot;&gt;在 Windows 10 上为 WPF 窗口添加模糊特效（就像开始菜单和操作中心那样） - walterlv&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.walterlv.com/post/using-acrylic-in-wpf-application.html&quot;&gt;在 WPF 程序中应用 Windows 10 真•亚克力效果 - walterlv&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.walterlv.com/post/set-window-composition-attribute&quot;&gt;使用 SetWindowCompositionAttribute 来控制程序的窗口边框和背景（可以做 Acrylic 亚克力效果、模糊效果、主题色效果等） - walterlv&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码参考:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/TwilightLemon/MyToolBar&quot;&gt;TwilightLemon/MyToolBar: 为Surface Pro而生的顶部工具栏 支持触控和笔快捷方式 (github.com)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sourcechord/FluentWPF&quot;&gt;sourcechord/FluentWPF: Fluent Design System for WPF. (github.com)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;注: 上面提到的文章参考可能只介绍到了 &quot;某一部分&quot; 内容, 可能会造成误导, 注意看完本篇文章后自行验证&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>[WPF] EleCho.WpfSuite, 控件, 布局容器, 过渡动画, 标记拓展以及实用工具</title><link>https://slimenull.com/posts/20240523162351/</link><guid isPermaLink="true">https://slimenull.com/posts/20240523162351/</guid><description>关于 WPF Suite 的介绍与使用方法</description><pubDate>Thu, 23 May 2024 16:23:51 GMT</pubDate><content:encoded>&lt;p&gt;相信各位朋友在搞 WPF 开发的时候，不可避免的会重复的写到很多重复的代码，以给WPF擦屁股。例如WPF中没有CornerRadiusAnimation，没有支持自定义的贝塞尔曲线过渡函数，内置的值转换器太少，控件无法设置圆角，容器不能直接通过自己的属性设置元素间距等诸多问题。&lt;/p&gt;
&lt;p&gt;于是，我写了个库，用于补全我认为 WPF 中应该有，但是缺失了的一些功能。&lt;/p&gt;
&lt;h2&gt;EleCho.WpfSuite&lt;/h2&gt;
&lt;p&gt;WPF 布局面板，控件，值转换器，标记拓展，过渡以及实用工具。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;你可以在 Releases 中下载 SampleApp 看看这个库大概是什么样子的。
https://github.com/OrgEleCho/EleCho.WpfSuite/releases&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h3&gt;安装与使用&lt;/h3&gt;
&lt;p&gt;在 nuget 中直接安装 EleCho.WpfSuite 即可。命名空间声明如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xmlns:ws=&quot;https://github.com/OrgEleCho/EleCho.WpfSuite&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;库的所有功能均在这个命名空间下。&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h3&gt;布局面板&lt;/h3&gt;
&lt;p&gt;WpfSuite 中重写了 StackPanel 以及 WrapPanel，它们现在均支持使用 “Spacing” 属性来直接调整元素之间的间距，而不需要手动设置元素 Margin。WrapPanel 的间距是允许自定义水平以及垂直两个方向上的。&lt;/p&gt;
&lt;p&gt;除此之外，WpfSuite 还提供了 FlexPanel（弹性面板），MasonryPanel（瀑布流面板）这两个布局容器。其中 FlexPanel 基本支持 CSS 中 flex 布局的所有功能，包括 Grow，Shrink 以及各种对齐。MasonryPanel 则是很直观的瀑布流布局，和 StackPanel 一样，你可以指定该容器的方向。而且这些布局控件都是支持使用 “Spacing” 属性来直接指定元素间距的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523162903.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h3&gt;控件&lt;/h3&gt;
&lt;p&gt;WpfSuite 重写了大部分常用 WPF 控件，为它们直接添加了 CornerRadius 属性，以及其他关于控件状态的相关属性，例如 “DisabledBackground” 属性，当为其制定值时，如果控件被禁用，那么控件的背景颜色就会切换到设置的颜色。其他的状态还包含 Hover，Pressed，Selected，Selected Inactive，这样我们不需要更改控件的模板，就能直接更改它在不同状态下的颜色，边框。&lt;/p&gt;
&lt;p&gt;很多时候我们会遇到 “控件” 的内容超出带圆角的 “Border” 范围的情况，我们希望控件的内容被裁剪到 Border 内部，现在这个需求可以非常简单的实现。WpfSuite 重写了 Border 的实现，并提供了一个 “ContentClip” 属性，只需要把 Border 的内容的 Clip 属性绑定到 Border 的 ContentClip 上，就可以完成自动裁剪了！&lt;/p&gt;
&lt;p&gt;而且，所有 WpfSuite 提供的控件均不影响原有 WPF 控件，它不会对原有 WPF 控件做任何更改，所以如果你担心某些控件的拓展功能会造成性能问题，你也可以完全不使用它。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523162925.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h3&gt;值转换器&lt;/h3&gt;
&lt;p&gt;WpfSuite 还提供了常用的诸多值转换器，例如用于将任意对象值判空并返回 bool 的转换器，将数字与指定数字做对比并返回 bool 的转换器，做一些简单的数学计算并返回结果的 bool 转换器，以及能够将值转换器串起来的 “值转换器组”。&lt;/p&gt;
&lt;p&gt;它们能够做的东西当然超级多，例如，如果你有一个属性是集合类型的，那么你完全可以将界面上某元素的 Visibility 绑定到这个属性的 Count 属性上，然后指定转换器为 “数字非0判断” 以及 Bool 转 Visibility 组合成的一个 ValueConverterGroup，于是，这个控件就仅在集合不为空的时候会显示出来了！这很 MVVM，这很酷！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ws:ValueConverterGroup x:Key=&quot;CollectionIsNotEmptyToVisibilityConverter&quot;&amp;gt;
    &amp;lt;ws:CollectionIsNotNullOrEmptyConverter/&amp;gt;
    &amp;lt;ws:BooleanToVisibilityConverter/&amp;gt;
&amp;lt;/ws:ValueConverterGroup&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523162944.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h3&gt;标记拓展&lt;/h3&gt;
&lt;p&gt;标记拓展应该是 WpfSuite 最简单的部分了，它只是提供了 MarkupExtension 以让你能在 XAML 中直接写出来表示具体类型的值。&lt;/p&gt;
&lt;p&gt;例如，你有一个控件，该控件有一个属性是 Object 类型的，你希望往里面传入一个长整型，如果你在 XAML 中直接写 “114514” 这样的数字，它肯定不会帮你转成你想要的类型并传入的，它会把你的字符串原封不动的传过去… 这个时候，标记拓展就派上用场了！你直接使用 {ws:Int64 114514} 就能直接表示一个精确的长整型的值。&lt;/p&gt;
&lt;p&gt;还有一些使用场景，就是 DataTrigger，例如你希望你的某些数据触发器，当数据等于某个确切值得时候触发，但很遗憾， DataTrigger 的 Value 属性正好是 Object 类型的，而有些时候它并不能识别到你这个数据的类型，也当然没办法对你在 XAML 中写的字符串做正确的转型，这个时候用能够制定确切类型的标记拓展再合适不过了。&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h3&gt;过渡&lt;/h3&gt;
&lt;p&gt;过渡在应用程序中用的应该是特别多的，我们很难想象，WPF 提供了完整的动画系统，但是居然没有提供封装好的过渡动画！&lt;/p&gt;
&lt;p&gt;有时候，我们希望某个内容被更改的时候，它能够执行一些动画，例如淡入淡出，划出划入，或者是缩放，甚至旋转，或把这些结合起来之类的，这个时候，WpfSuite 提供的 TransitioningContentControl 能够轻松的帮你解决这个问题！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;熟悉 Avalonia 的朋友肯定要说了，这不就 Avalonia 自带的东西嘛！没错！我就是看到 Avalonia 有这东西，但是 WPF 没有，然后我眼红，就移植过来了！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对 TransitioningContentControl 指定你想要的过渡效果，它就可以在内容变更时自动执行这个效果。内置的有 Slide（滑动），Fade（淡入淡出），Scale（缩放）以及结合 Slide 和 Fade 的 SlideFade（滑动的同时，淡入淡出）。&lt;/p&gt;
&lt;p&gt;而且我们对 Frame 控件也支持了过渡动画，又因为动画也是有正向与反向之分的，所以当你使用 Frame 的 ”GoBack“ 方法时，动画的执行也是反向的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523164142.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同时，你也可以手动将动画的 Reverse 设为 true，这样它的方向就被置换了。最经典的一个应用就是，左侧导航项与右侧带滑动动画的 Frame，当点击当前项后面的项时，正向执行动画，当点击当前项前面的项时，反向执行动画，这样你就做到了一个非常不错的页面滑动效果！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523164633.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523164706.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;除此之外，你也可以用它来做 Banner 图像的切换动画，例如用户点击按钮的时候，更换图片，而图片由 TransitioningContentControl 容纳，所以也可与用它来做这个动画，点击左边的按钮则从左方滑入新图片，点击右边的按钮则从右方滑入新图片，听起来就很不错。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523181634.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h3&gt;实用工具&lt;/h3&gt;
&lt;p&gt;在这部分中，最有用的就属绑定代理了。举个例子，我需要做一个列表项的右键菜单，并且菜单使用 MVVM，也就是说，逻辑使用指令实现，参数通过绑定传入。但是右键菜单的可视化树和列表不是同一个，你在 Menu 上使用 Binding 会因为找不到 DataContext 而失败。&lt;/p&gt;
&lt;p&gt;使用绑定代理则可以轻松解决这个问题。因为静态资源是正常使用的，所以你只需要把绑定代理作为静态资源定义在窗口中，并指定 Data=&quot;{Binding}&quot;，然后在你的 Menu 中把 Binding 的 DataSource 指定为刚刚定义的静态资源，就能够成功在菜单中引用到正常的 DataContext 了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523163503.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;该方案适用于任何因 VisualTree 而导致 DataContext 找不到，进而导致绑定失败的所有问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;除此之外，还有一些小工具，例如 ValidationUtils，用于自定义依赖属性的属性验证，或者 ColorUtils，用于计算 HSV 与 RGB 的转换。&lt;/p&gt;
</content:encoded></item><item><title>Unity 小地图, 实时与静态, 地图标记, 点击导航, 路线显示, 风格化</title><link>https://slimenull.com/posts/20240306132352/</link><guid isPermaLink="true">https://slimenull.com/posts/20240306132352/</guid><description>Unity 制作小地图, 支持实时相机渲染, 静态贴图, 地图标记, 点击地图导航, 显示导航路线, 风格化小地图</description><pubDate>Wed, 06 Mar 2024 13:23:52 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/images/vlcsnap-2024-03-08-11h04m42s227.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如上图, 这些地图分别是:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;位置和方向均跟随玩家, 并且带有地图标记的地图&lt;/li&gt;
&lt;li&gt;位置和方向固定, 带有地图标记的地图&lt;/li&gt;
&lt;li&gt;位置跟随玩家, 带有地图标记的地图&lt;/li&gt;
&lt;li&gt;位置跟随玩家, 带有部分地图标记, 能够显示导航路线的地图&lt;/li&gt;
&lt;li&gt;位置跟随玩家, 带有部分地图标记, 能够显示导航路线且风格化的地图.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h2&gt;实时地图&lt;/h2&gt;
&lt;p&gt;在 Unity 中, 相机渲染的结果能够直接保存到 &quot;渲染贴图&quot; 中, 而贴图可以呈现在 UI 界面上, 通过这一方式, 我们可以创建最简单的实时地图.&lt;/p&gt;
&lt;p&gt;首先, 在项目面板中, 我们需要创建一个用于存储相机渲染输出的 &quot;渲染贴图&quot;, 右击鼠标, 选择 &quot;创建 -&amp;gt; 渲染贴图(Render texture)&quot;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240308111441.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后, 在创建中创建一个垂直向下看的相机, 并将投影模式改为 &quot;正交(Orthographic)&quot;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240308111814.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后, 将相机的 &quot;目标贴图&quot; 设置为我们刚刚创建好的渲染贴图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240308112025.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;做完这些操作之后, 你会得到这样的结果:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240308112212.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后, 我们只需要使用任意方式, 将贴图展示在屏幕上就可以了. 最简单的方式就是直接使用 UI 中的 &quot;RawImage&quot; 进行显示.&lt;/p&gt;
&lt;p&gt;在场景中创建 RawImage, 然后将其要显示的纹理设置为我们创建好的渲染纹理, 最后你会看到, UI 上的图片显示着相机拍摄到的内容.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240308112626.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;至此, 最简单的实时小地图, 我们就做好了. 关于一些简单的拓展的内容, 大概如下:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果要跟随玩家, 那么直接手写脚本, 使相机跟随玩家即可&lt;/li&gt;
&lt;li&gt;如果要随玩家旋转, 直接手写脚本, 跟随玩家, 并且跟随玩家旋转. 当然你也可以直接将其设置为玩家的子对象, 这样玩家移动和旋转的时候, 相机也会移动和旋转.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h2&gt;静态地图&lt;/h2&gt;
&lt;p&gt;静态地图就是提前对场景进行拍摄, 拿到贴图之后放到 UI 上, 然后在实际显示时, 根据比例系数, 移动这张图片.&lt;/p&gt;
&lt;p&gt;在 Unity 中截图还是比较麻烦的, 建议是写个编辑器脚本, 点击按钮, 动态的创建相机, 然后使用正交相机, 渲染到 RenderTexture, 再将 RenderTexture 保存到本地, 下面是供参考的代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MinimapTextureTool : EditorWindow
{
    [MenuItem(&quot;Tools/MinimapTextureTool&quot;)]
    static void Open()
    {
        var window = EditorWindow.GetWindow&amp;lt;MinimapTextureTool&amp;gt;();
        window.Show();
    }
    
    string _textureSizeStr = &quot;1024&quot;;
    string _cameraHeightStr = &quot;5&quot;;
    string _minimapAreaSizeStr = &quot;50&quot;;
    int _minimapLayers = ~0;

    void OnGUI()
    {
        GUILayout.Label(&quot;纹理工具&quot;);

        GUILayout.Label(&quot;纹理大小: &quot;);
        _textureSizeStr = GUILayout.TextField(_textureSizeStr);

        GUILayout.Label(&quot;相机高度: &quot;);
        _cameraHeightStr = GUILayout.TextField(_cameraHeightStr);

        GUILayout.Label(&quot;小地图区域大小: &quot;);
        _minimapAreaSizeStr = GUILayout.TextField(_minimapAreaSizeStr);

        GUILayout.Label(&quot;小地图层级遮罩: &quot;);
        LayerMask tempMask = EditorGUILayout.MaskField( InternalEditorUtility.LayerMaskToConcatenatedLayersMask(_minimapLayers), InternalEditorUtility.layers);
        _minimapLayers = InternalEditorUtility.ConcatenatedLayersMaskToLayerMask(tempMask);

        GUILayout.Space(10);

        if (!int.TryParse(_textureSizeStr, out var textureSize) ||
            !float.TryParse(_cameraHeightStr, out var cameraHeight) ||
            !float.TryParse(_minimapAreaSizeStr, out var minimapAreaSize))
        {
            GUILayout.Label(&quot;输入有误!&quot;);
            return;
        }

        if (GUILayout.Button(&quot;生成小地图纹理&quot;))
        {
            string filename = EditorUtility.SaveFilePanel(&quot;保存纹理&quot;, Application.dataPath, &quot;小地图&quot;, &quot;png&quot;);

            // 创建渲染纹理
            RenderTexture rt = new RenderTexture(textureSize, textureSize, 32);

            // 创建相机
            GameObject cameraGameObject = new GameObject();
            Camera camera = cameraGameObject.AddComponent&amp;lt;Camera&amp;gt;();

            cameraGameObject.transform.eulerAngles = new Vector3(90, 0, 0);
            cameraGameObject.transform.position = new Vector3(0, cameraHeight, 0);

            // 设置相机属性
            camera.cullingMask = _minimapLayers;
            camera.clearFlags = CameraClearFlags.SolidColor;
            camera.backgroundColor = new Color(0, 0, 0, 0);
            camera.orthographic = true;
            camera.orthographicSize = minimapAreaSize / 2;

            // 渲染
            camera.targetTexture = rt;
            camera.Render();

            // 保存当前激活的渲染纹理, 然后激活刚创建的渲染纹理
            var activeRenderTextureBefore = RenderTexture.active;
            RenderTexture.active = rt;

            // 创建 2D 纹理, 并从渲染纹理中读入数据
            Texture2D png = new Texture2D(textureSize, textureSize);
            png.ReadPixels(new Rect(0, 0, textureSize, textureSize), 0, 0);
            png.Apply();

            // 恢复之前激活的渲染纹理
            RenderTexture.active = activeRenderTextureBefore;

            // 编码 PNG, 并保存
            var bytes = png.EncodeToPNG();
            System.IO.File.WriteAllBytes(filename, bytes);

            // 清理临时创建的对象
            DestroyImmediate(cameraGameObject);
            DestroyImmediate(rt);
            DestroyImmediate(png);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要手动截图的话, 可以将窗口上的这两个关闭, 隐藏天空盒以及辅助线之类的.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240311112837.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里以一个准备好的地图做示例.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240311145121.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后, 我们继续写一个简单的脚本用来展示地图, 通过改变顶点 UV, 实现移动地图内容.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class SimpleTextureMinimap : MaskableGraphic
{
    public Texture MinimapTexture;
    public Vector2 Pivot = new Vector2(.5f, .5f);

    public float MinimapAreaSize = 50;
    public float DisplayAreaSize = 10;
    public float ImageSize = 100;

    public Transform FollowTarget;

    private void Update()
    {
        if (FollowTarget != null)
            SetVerticesDirty();
    }

    public override Texture mainTexture =&amp;gt; MinimapTexture;

    protected override void OnPopulateMesh(VertexHelper vh)
    {
        vh.Clear();

        var radius = ImageSize / 2;
        var uvRadius = DisplayAreaSize / MinimapAreaSize / 2;
        var uvOffset = new Vector2();

        if (FollowTarget != null)
            uvOffset = new Vector2(FollowTarget.position.x, FollowTarget.position.z) / MinimapAreaSize;

        vh.AddVert(new Vector3(-radius, -radius), color, Pivot + uvOffset + new Vector2(-uvRadius, -uvRadius));
        vh.AddVert(new Vector3(-radius, radius), color, Pivot + uvOffset + new Vector2(-uvRadius, uvRadius));
        vh.AddVert(new Vector3(radius, radius), color, Pivot + uvOffset + new Vector2(uvRadius, uvRadius));
        vh.AddVert(new Vector3(radius, -radius), color, Pivot + uvOffset + new Vector2(uvRadius, -uvRadius));

        vh.AddTriangle(0, 1, 2);
        vh.AddTriangle(0, 2, 3);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的代码中, 我们只使用跟随目标的位置计算 UV 偏移量, 没有考虑到当玩家旋转时, 地图也旋转的这种需求, 所以计算就变得简单了许多.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;地图标记&lt;/h2&gt;
&lt;p&gt;要将场景中的某些物体在地图上标记出来, 方式有很多. 对于基于渲染相机的小地图, 其中最简单的方式是直接在物体上创建一个世界坐标空间的 Canvas, 并在上面放一张图片, 然后让小地图相机拍摄到.&lt;/p&gt;
&lt;p&gt;就像这样, 调整 Canvas 的位置, 旋转, 缩放使其面朝上方以被相机地图拍摄到, 然后设置单独的层级, 并且使主相机不渲染该层级, 而小地图相机渲染它.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240311155359.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后的效果, 就是这样的:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240311155649.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于静态地图, 也就是地图由贴图构成, 我们需要做世界坐标到地图坐标的坐标转换, 然后在地图上实例化 Sprite, 并设置其位置.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;地图标记当然不止这一种实现方式, 不如说, 直接用 UI 在小地图上放一个图片是更好的解决方案&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;小地图包&lt;/h2&gt;
&lt;p&gt;对于更多功能, 我将它封装成一个 Unity 包了. 其中就包含实时小地图, 静态小地图, 标记, 点击导航以及路线显示.&lt;/p&gt;
&lt;p&gt;要安装这个包, 首先确保你的电脑中有安装 &apos;git&apos;, 然后在 Unity 包管理器中, 单击左上角的 &apos;添加包&apos; 按钮, 然后单击 &apos;从 git URL 添加包&apos;, 然后输入 &lt;code&gt;https://github.com/SlimeNull/UnityMinimaps.git&lt;/code&gt; 并确认.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240517105319.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240517105207.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不出意外的话, 名为 &quot;SlimeNull.UnityMinimaps&quot; 的包会被安装到你的项目中:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240517105433.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在, 我们可以直接在场景中创建小地图了. 在 Unity 菜单栏中, 找到 &lt;code&gt;Tools&lt;/code&gt; 按钮, 单击其中的 &lt;code&gt;Minimap&lt;/code&gt; 即可打开小地图创建工具.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240517105647.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果要在场景中创建一个基于相机与渲染纹理的实时地图, 需要以下准备:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一个渲染纹理&lt;/li&gt;
&lt;li&gt;一个地图相机, 并且有赋值上面所说的渲染纹理, 同时该相机必须正向垂直向下照射(X旋转90度, Y 和 Z 不旋转)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523100248.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后, 将小地图类型调整为 &quot;Render texture&quot;, 拖拽小地图相机到窗口中, 最后在场景层级中选择你要放置小地图的位置(这里是 Canvas 节点), 然后点击 &quot;创建&quot; 即可.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你在场景层级中选择对象, 它会提示你 &quot;选择一个在 Canvas 之下的 GameObject 以创建小地图&quot;, 只有当你选择之后, 才会有 &quot;创建&quot; 按钮.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523091600.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在做完上述步骤之后, Canvas 下就会自动创建好一个小地图, 其中显示的内容是相机所照射到的内容. 通过这一方式就能实现实时小地图了.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523091848.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果你要调整小地图的显示大小, 则需要设置 &lt;code&gt;Minimap&lt;/code&gt; 组件中的 &lt;code&gt;Size&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523092024.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;操作&lt;/h3&gt;
&lt;p&gt;通过更改 &lt;code&gt;Minimap&lt;/code&gt; 组件中的 &lt;code&gt;AreaSize&lt;/code&gt;, 可以指定当前小地图显示的区域大小. 不过, 注意不要超过小地图相机的照射范围, 因为范围之外是显示不到的.&lt;/p&gt;
&lt;p&gt;自带的 &lt;code&gt;MinimapModifier&lt;/code&gt; 组件是实现用户鼠标滚轮缩放小地图的功能的. &lt;code&gt;Factor&lt;/code&gt; 用来指定缩放的速度倍数, &lt;code&gt;MinSize&lt;/code&gt; 和 &lt;code&gt;MaxSize&lt;/code&gt; 则是最小与最大的大小. 如果你不希望用户能够通过鼠标滚轮调整大小, 移除这个组件就可以了.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523095045.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;人物跟随&lt;/h3&gt;
&lt;p&gt;要实现人物跟随, 只需要将要跟随的物体设置到 &lt;code&gt;Minimap&lt;/code&gt; 组件的 &lt;code&gt;Origin&lt;/code&gt; 上即可. 并且, 当跟随的物体旋转时, 小地图也会跟着旋转, 保证玩家的前方始终是地图上方.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523092355.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不过, 需要注意的是, 如果你的小地图相机不会移动, 那么当玩家超出小地图照射范围的时候, 那么小地图的内容也会变成空, 因为小地图渲染器获取不到玩家周围的地图信息了.&lt;/p&gt;
&lt;p&gt;所以一般的, 如果你需要始终能够正确显示小地图, 你还需要保证小地图跟随玩家, 以保证小地图组件能够获取到玩家周围的地图信息. 我们需要一个简单的物体跟随脚本来使相机始终跟随玩家.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using UnityEngine;

[ExecuteAlways]
public class FollowObject : MonoBehaviour
{
    public Transform Target;
    public Vector3 Offset;

    private void LateUpdate()
    {
        if (Target != null)
        {
            transform.position = Target.position + Offset;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后挂载到小地图相机上, 这样小地图就始终跟随玩家了.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523100417.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意, 不要直接将小地图相机挂载到玩家下, 因为这样, 玩家的旋转会影响到地图, 从而导致地图错乱, 以下的做法是 &lt;strong&gt;错误&lt;/strong&gt; 的!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523100623.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;多边形地图&lt;/h3&gt;
&lt;p&gt;默认小地图的显示是 &quot;方形&quot; 的, 如果你需要换成多边形的小地图, 只需要将小地图中的 &quot;MinimapRenderer&quot; 组件换成 &quot;MinimapPolygonRenderer&quot; 即可.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523092958.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523093035.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过增加 &quot;顶点数&quot; 你也可以将它做成近似 &quot;圆形&quot; 的小地图.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523093233.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当然, 如果你需要更多奇奇怪怪形状的小地图, 也可以直接使用 UI 上的遮罩, 毕竟小地图的渲染器也是一个 MaskableGraphics.&lt;/p&gt;
&lt;h3&gt;标记&lt;/h3&gt;
&lt;p&gt;上面我们讲了直接通过让渲染贴图照射到一个 UI 图像来实现的小地图, 但在这里的小地图包中, 直接提供了在小地图 UI 上显示标记的功能.&lt;/p&gt;
&lt;p&gt;首先我们需要把我们的 &quot;标记&quot; 做成预制体, 这通常是一个 &quot;Image&quot; 图片.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523094335.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后在小地图的 &quot;MinimapIndicator&quot; 组件中添加一个新项, 并指定要标记的物体, 以及图标的预制体即可.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523094457.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在运行时, 小地图标记就会实例化到小地图中, 然后显示出来, 并跟随目标物体.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523094631.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;导航&lt;/h3&gt;
&lt;p&gt;这个小地图包还支持用户点击小地图时, 调用主角的 &lt;code&gt;NavMeshAgent&lt;/code&gt; 进行导航. 只需要将玩家的 &lt;code&gt;NavMeshAgent&lt;/code&gt; 赋值到小地图下的 &lt;code&gt;MinimapNavigator&lt;/code&gt; 组件的 &lt;code&gt;NavMeshAgent&lt;/code&gt; 上, 然后设置能够导航的层级遮罩即可.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523100906.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在, 在地图上点击, 小地图的导航器就会自动做射线检测, 找到目标点, 并设置玩家的 &lt;code&gt;NavMeshAgent&lt;/code&gt; 的目标点.&lt;/p&gt;
&lt;p&gt;如果你还想要在地图上显示导航路径, 可以用小地图下面的 &lt;code&gt;NavigationPathRenderer&lt;/code&gt;, 默认创建的小地图会包含一个, 将需要显示导航路径的 &lt;code&gt;NavMeshAgent&lt;/code&gt; 赋值上去就可以了.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523101218.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;风格化&lt;/h3&gt;
&lt;p&gt;风格化在这里实现的原理就是直接做好场景的一个地图图片, 然后小地图在渲染时, 就基于这个小地图的内容进行渲染. 和实时小地图一样, 你不需要自己编写任何逻辑, 只需要跟随向导, 拍摄一个地图图片, 然后用美术软件对其进行处理, 最后设置好相关属性就可以了.&lt;/p&gt;
&lt;p&gt;使用小地图工具中的 &quot;纹理工具&quot; 可以直接拍摄一张当前场景的地图图片, 设定好你想要的纹理大小, 用于拍摄的相机高度, 小地图的区域大小(直径), 以及要拍摄的层级, 最后点击 &quot;生成小地图纹理&quot; 即可.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523101903.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后使用编辑好的地图贴图创建一个静态地图就可以了.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523102150.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;微调设置, 最后的效果大致如此. 可以看到在静态的小地图上, 有我们通过绘图工具绘制的一些线条.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/Pasted%20image%2020240523102350.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是, 在创建静态小地图时, 纹理的区域大小必须设置正确, 否则小地图的显示就出问题了. 所以建议对于小地图纹理的命名中包含小地图的大小, 这样就不会忘记地图大小了.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;另外, 由于静态小地图的渲染基于静止的图片, 所以场景中运动的物体, 你就没办法实时的显示出来它了, 而是应该使用 &quot;地图标记&quot; 来表示它的位置与方向.&lt;/p&gt;
</content:encoded></item><item><title>Unity 轮转图, 惯性, 自动回正, 点击选择</title><link>https://slimenull.com/posts/20240226141713/</link><guid isPermaLink="true">https://slimenull.com/posts/20240226141713/</guid><description>类似于网页中轮播图的, 2D 与 3D 的轮转图, 包含拖拽的惯性, 拖拽结束的自动回正以及点击选择功能</description><pubDate>Mon, 26 Feb 2024 14:17:13 GMT</pubDate><content:encoded>&lt;p&gt;简单的实现 2D 以及 3D 的轮转图, 类似于 Web 中无限循环的轮播图那样.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/unity_carousel_2d.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/unity_carousel_3d.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;文中所有代码均已同步至 &lt;a href=&quot;https://github.com/SlimeNull/UnityTests&quot;&gt;github.com/SlimeNull/UnityTests&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3D 轮转图: &lt;a href=&quot;https://github.com/SlimeNull/UnityTests/blob/main/Assets/Scripts/Scenes/CarouselTestScene/Carousel.cs&quot;&gt;Assets/Scripts/Scenes/CarouselTestScene/Carousel.cs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;2D 轮转图: &lt;a href=&quot;https://github.com/SlimeNull/UnityTests/blob/main/Assets/Scripts/Scenes/CarouselTestScene/UICarousel.cs&quot;&gt;Assets/Scripts/Scenes/CarouselTestScene/UICarousel.cs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;主要逻辑&lt;/h2&gt;
&lt;p&gt;根据我们想要的效果可知, 主要原理就是, 当鼠标拖拽时, 更新对象的位置. 我们可以通过在脚本中保存一个 &quot;当前旋转量&quot; 的变量, 当鼠标拖拽时, 更改它. 而每次拖拽, 都根据旋转量, 计算每一个对象的位置. 接下来以 3D 轮转图为例, 讲述编写思路.&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h3&gt;基础旋转&lt;/h3&gt;
&lt;p&gt;设计对象层级关系:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- 父对象 (脚本挂载到这里)
  |- 子对象
  |- 子对象
  |- 子对象
  |- ...
  |- 子对象
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;轮转图的大概逻辑框架:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Carousel : MonoBehavior, IDragHandler
{
    // 当前旋转偏移量
    float _radianOffset;

    void Start()
    {
        // 在开始时, 更新物体状态
        UpdateObjectStatus();
    }

    private void UpdateObjectStatus()
    {
        // 更新子物体状态, 计算旋转, 设置位置等
    }

    void IDragHandler.OnDrag(PointerEventData eventData)
    {
        var mouseOffset = 获取鼠标拖拽偏移量的逻辑;
	    var radianChange = 将鼠标偏移量转换为旋转量的逻辑;
	    
        _radianOffset += radianChange;
        UpdateObjectStatus();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写从旋转角度, 更新物体状态的逻辑. 这里, 我们将一整个圆平均分为成员数量份, 并让它们平均分布. 半径的话, 我们允许用户在检视器中自定义, 所以在这里向外暴露字段即可.&lt;/p&gt;
&lt;p&gt;需要注意的是, 在数学习惯三角函数中, 角的起始为 x 正方向, 也就是 &quot;右方&quot;, 为了使第一个子物体, 也就是旋转量为 0 的子物体在最前方, 我们在实际计算角度时, 需要整体减去半个 PI, 也就是 90 度.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/circle_radian_rotation.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Carousel : MonoBehavior, IDragHandler
{
    float _radianOffset;

	/// &amp;lt;summary&amp;gt;
	/// 大小
	/// &amp;lt;/summary&amp;gt;
	[field: SerializeField]
	public float Size { get; set; } = 5;

    void Start()
    {
        UpdateObjectStatus();
    }

    // 更新物体状态
    private void UpdateObjectStatus()
    {
		var radius = Size / 2;                        // 半径
		var childCount = transform.childCount;        // 成员数量
		var radianGap = Mathf.PI * 2 / childCount;    // 旋转弧度间隔

		for (int i = 0; i &amp;lt; childCount; i++)
		{
		    // 获取当前角度, 以及当前游戏对象
			var radian = radianGap * i + _radianOffset - Mathf.PI / 2;
			var child = transform.GetChild(i);

            // 计算余弦正弦值
			var cos = Mathf.Cos(radian);
			var sin = Mathf.Sin(radian);

            // 计算坐标
			var x = cos * radius;
			var z = sin * radius;

            // 设置位置
			child.localPosition = new Vector3(x, 0, z);
		}
    }

    void IDragHandler.OnDrag(PointerEventData eventData)
    {
        // 拖拽逻辑
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来实现获取鼠标拖拽偏移量, 由于 3D 空间中的对象, 距离相机远近不确定, 所以最终的鼠标偏移量我们以鼠标位置转换为世界坐标的结果为准. 在开始拖拽的时候, 记录下当前距离相机的位置与鼠标的世界坐标, 并在接下来的每一次拖拽触发时都重新计算鼠标的世界坐标, 并得出鼠标在世界中的偏移量.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Carousel : MonoBehavior, IDragHandler, IBeginDragHandler
{
    float _radianOffset;
    
    float _cameraDistance;              // 相机距离
    Vector3 _lastMouseWorldPosition;    // 上次的鼠标世界坐标

	/// &amp;lt;summary&amp;gt;
	/// 大小
	/// &amp;lt;/summary&amp;gt;
	[field: SerializeField]
	public float Size { get; set; } = 5;

    void Start()
    {
        UpdateObjectStatus();
    }

    // 更新物体状态
    private void UpdateObjectStatus()
    {
        // 位置更新逻辑
    }

    void IDragHandler.OnDrag(PointerEventData eventData)
    {
        var radius = Size / 2;

        // 求当前鼠标坐标, 计算鼠标的偏移量
        var mousePosition = Input.mousePosition;
        mousePosition.z = _cameraDistance;
        var newMouseWorldPosition = Camera.main.ScreenToWorldPoint(mousePosition);
        var mouseWorldOffset = newMouseWorldPosition - _lastMouseWorldPosition;

        // 保存新的鼠标位置
        _lastMouseWorldPosition = newMouseWorldPosition;

        // 将鼠标偏移量转换为旋转量
        var radianChange = mouseWorldOffset.x / radius;

        // 增加旋转
        _radianOffset += radianChange;

        // 更新物体状态
        UpdateObjectStatus();
    }

    void IBeginDragHandler.OnBeginDrag(PointerEventData eventData)
    {
        // 通过自己的屏幕坐标, 取 z 值得到相机距离
        // 当然, 这里你也可以使用向量投影来直接获取当前对象与相机的距离
        var selfScreenPosition = Camera.main.WorldToScreenPoint(transform.position);
        _cameraDistance = selfScreenPosition.z;

        // 求鼠标的世界坐标
        var mousePosition = Input.mousePosition;
        mousePosition.z = _cameraDistance;
        _lastMouseWorldPosition = Camera.main.ScreenToWorldPoint(mousePosition);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的代码中, 我们的旋转量是直接将鼠标偏移量除以半径得来的. 因为圆的周长等于弧乘半径, 反之, 如果想要根据圆上长度求角大小, 直接拿这个长度除以半径即可. 而我们这里的思路, 正是将鼠标的偏移量, 视作圆上一点的旋转距离.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/circle_length_equals_lr.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当然, 你也可以直接通过别的算法来将鼠标偏移量转换为旋转量, 这取决于你的需求. 甚至你也可以直接暴力的将鼠标的坐标偏移乘或除以一个固定的数来作为旋转量使用.&lt;/p&gt;
&lt;p&gt;最后, 检查一下你的代码, 它们大概是这样的:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Carousel : MonoBehavior, IDragHandler, IBeginDragHandler
{
    float _radianOffset;
    
    float _cameraDistance;
    Vector3 _lastMouseWorldPosition;

    public float Size { get; set; } = 5;

    void Start() { ... }

    private void UpdateObjectStatus() { ... }

    void IDragHandler.OnDrag(PointerEventData eventData) { .. }
    void IBeginDragHandler.OnBeginDrag(PointerEventData eventData) { ... }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在编写完基础代码之后, 我们的效果大概如下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/basic_carousel_3d.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意, 为了让 3D 场景中的物体能够接收指针拖拽的事件, 你的场景中需要有一个 &quot;EventSystem&quot; 组件, 并且主相机需要挂载 &quot;GraphicsRaycaster&quot; 组件.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h3&gt;自动回正&lt;/h3&gt;
&lt;p&gt;接下来我们还需要继续编写自动回正的功能, 使最近的物体旋转到前方. 大概思路就是, 保存当前最靠近前方的物体索引, 并计算其在最前方的旋转偏移量, 最后使用 DOTween 执行缓动.&lt;/p&gt;
&lt;p&gt;因为我们使用正弦值乘半径作为物体的 z 坐标, 所以对于获取当前最前方的物体, 我们只需要看哪个物体旋转量正弦值最小即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Carousel : MonoBehavior, IDragHandler, IBeginDragHandler
{
    // 其他字段和属性

    /// &amp;lt;summary&amp;gt;
    /// 已选择的索引
    /// &amp;lt;/summary&amp;gt;
    public SelectedIndex { get; private set; }

    // 更新物体状态
    private void UpdateObjectStatus()
    {
		var radius = Size / 2;
		var childCount = transform.childCount;
		var radianGap = Mathf.PI * 2 / childCount;

        var minSin = 2f;
        var selectedIndex = -1;

		for (int i = 0; i &amp;lt; childCount; i++)
		{
			var radian = radianGap * i + _radianOffset - Mathf.PI / 2;
			var child = transform.GetChild(i);

			var cos = Mathf.Cos(radian);
			var sin = Mathf.Sin(radian);

			var x = cos * radius;
			var z = sin * radius;

			child.localPosition = new Vector3(x, 0, z);

            // 如果当前正弦值小于已保存的最小正弦值
            // 则更新最小正弦值以及已选中索引
            if (sin &amp;lt;= minSin)
            {
                minSin = sin;
                selectedIndex = i;
            }
		}

        // 更新属性
        SelectedIndex = selectedIndex;
    }

    // 其他逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要获取某个子对象对应的旋转角度, 直接拿旋转间隔, 乘其索引, 再转为负数即可. 因为从第一个元素旋转到第二个元素时, 旋转角度是减少的.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/unity_carousel_rotation2.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Carousel : MonoBehavior, IDragHandler, IBeginDragHandler
{   
    // 其他字段和属性

    /// &amp;lt;summary&amp;gt;
    /// 从索引获取旋转量
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;index&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    private float GetRadianFromItemIndex(int index)
    {
        var childCount = transform.childCount;
        var radianGap = Mathf.PI * 2 / childCount;
    
        return -radianGap * index;
    }


    // 其他逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么, 选中某个元素, 将其移动到最前方的逻辑, 大概就是这样了, 使用 DOTween 做过渡就可以:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Carousel : MonoBehavior, IDragHandler, IBeginDragHandler
{
    float _radianOffset;

    // 其他字段

    /// &amp;lt;summary&amp;gt;
    /// 过渡时间
    /// &amp;lt;/summary&amp;gt;
    [field: SerializeField]
    public float TransitionTime { get; set; } = 0.2f;
    
    // 其他字段和属性
    
    // 更新物体状态
    private void UpdateObjectStatus() { ... }

    // 从索引获取旋转角度
    private float GetRadianFromItemIndex(int index) { ... }

    public void Select(int index)
    {
        var originRadian = _radianOffset;
        var targetRadian = GetRadianFromItemIndex(index);

        DOTween
            .To(radian =&amp;gt;
            {
                // 设置旋转量并更新物体状态
                _radianOffset = radian;
                UpdateObjectStatus();
            }, originRadian, targetRadian, TransitionTime);
    }

    // 其他逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过我们接下来还需要考虑两件事.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果我当前旋转量是一圈多, 过渡会出问题.
举个例子, 当旋转量是两圈半时, 也就是 5 个 PI, 当我调用 Select(0) 的时候, 它会直接从 5 个 PI 过渡到 0, 在视觉上就是, 这个轮转图转了两圈半, 回到原点.&lt;/li&gt;
&lt;li&gt;如果当前旋转到目标旋转量大于半个圆, 视觉上不好看.
举个例子, 我需要从 180 度旋转到 370 度, 如果直接这么转, 视觉上看起来是它绕了远路, 因为从 180 转到 10 度经过的 170 当然是比 转到 370 经过的 190 度要短的.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以, 我们需要做两点修改:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使当前旋转量不能大于一圈.&lt;/li&gt;
&lt;li&gt;在进行旋转前, 将目标旋转值改为最短的目标值.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中第一点非常简单, 只需要在每一次更新物体状态前, 对当前旋转量 &lt;code&gt;_radianOffset&lt;/code&gt; 对 &lt;code&gt;PI * 2&lt;/code&gt; 进行求余即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Carousel : MonoBehavior, IDragHandler, IBeginDragHandler
{   
    // 字段和属性

    // 更新物体状态
    private void UpdateObjectStatus()
    {
        // 变量初始化

        // 在执行操作前, 保证 _radianOffset 不大于一圈
        _radianOffset %= Mathf.PI * 2;
		for (int i = 0; i &amp;lt; childCount; i++)
		{
            // 更新物体位置以及其他的具体逻辑
		}

        // 更新属性
        SelectedIndex = selectedIndex;
    }


    // 其他逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二点, 我们只需要判断旋转差值, 如果大于半圆, 那么加上或者减去一整个圆即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Carousel : MonoBehavior, IDragHandler, IBeginDragHandler
{   
    // 字段和属性

    // 矫正旋转目标
    private void CorrectRotationTarget(float origin, ref float target)
    {
        const float DoublePI = Mathf.PI * 2;

        // 旋转差值
        float diff = (target - origin) % DoublePI;
        if (diff &amp;gt; Mathf.PI)
            diff -= DoublePI;
        else if (diff &amp;lt; -Mathf.PI)
            diff += DoublePI;
    
        target = origin + diff;
    }


    // 其他逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后, 我们在松开鼠标, 也就是结束拖拽的时候, 调用 &lt;code&gt;Select&lt;/code&gt; 方法, 选择当前最前方物体即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Carousel : MonoBehavior, IDragHandler, IBeginDragHandler, IEndDragHandler
{   
    // 其他字段和属性

    /// &amp;lt;summary&amp;gt;
    /// 启用自动回正
    /// &amp;lt;/summary&amp;gt;
    [field: SerializeField]
    public bool EnableAutoCorrection { get; set; } = true;

    // 选择
    public void Select(int index) { ... }

    void IEndDragHandler.OnEndDrag(PointerEventData eventData)
    {
        if (EnableAutoCorrection)
        {
            Select(SelectedIndex);
        }
    }

    // 其他逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后, 检查你的代码, 它们大概是这样的:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Carousel : MonoBehavior, IDragHandler, IBeginDragHandler, IEndDragHandler
{
    float _radianOffset;
    
    float _cameraDistance;
    Vector3 _lastMouseWorldPosition;

    public float Size { get; set; } = 5;
    public bool EnableAutoCorrection { get; set; } = true;
    public float TransitionTime { get; set; } = 0.2f;

    public int SelectedIndex { get; private set; }

    void Start() { ... }

    private void UpdateObjectStatus() { ... }
    private void CorrectRotationTarget(float origin, ref float target) { ... }
    private float GetRadianFromItemIndex(int index) { ... }

    public void Select(int index) { ... }

    void IDragHandler.OnDrag(PointerEventData eventData) { .. }
    void IBeginDragHandler.OnBeginDrag(PointerEventData eventData) { ... }
    void IEndDragHandler.OnEndDrag(PointerEventData eventData) { ... }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在做完上面一切工作之后, 我们最终会有一个带自动回正的轮转图. 效果如下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/unity_basic_carousel_auto_correction.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h3&gt;运动惯性&lt;/h3&gt;
&lt;p&gt;惯性的实现也是很简单的, 我们只需要保存拖拽时的速度, 在松开时使其仍然继续运动, 并不断减小速度就可以.&lt;/p&gt;
&lt;p&gt;我们的大概思路如下:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用一个字段保存速度, 一个字段保存当前是否正在被用户拖拽. 向外公开 &quot;旋转阻力&quot; 以允许在检视器中调整阻力.&lt;/li&gt;
&lt;li&gt;在 Update 中, 如果启用了惯性, 没有被用户拖拽, 并且速度不为 0, 则通过速度对旋转量进行增加, 并更新物体状态. 如果在一次更新中, 速度最终衰减到了 0, 那么则执行自动回正的逻辑.&lt;/li&gt;
&lt;li&gt;在 Drag 中, 根据当前拖拽的旋转增量以及时间差, 计算并保存旋转速度.&lt;/li&gt;
&lt;li&gt;在 EndDrag 中, 原本直接调用自动回正的逻辑, 仅在不启用惯性的时候直接执行. 因为如果启动了惯性, 在 Update 中会执行自动回正, 不需要在结束拖拽时执行.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终, 代码的框架大致如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Carousel : MonoBehavior, IDragHandler, IBeginDragHandler, IEndDragHandler
{
    // 其他字段与属性

    float _radianVelocity;   // 旋转速度

    bool _dragging;          // 是否正在被拖拽

    /// &amp;lt;summary&amp;gt;
    /// 旋转阻力
    /// &amp;lt;/summary&amp;gt;
    [field: SerializeField]
    public float RadianDrag { get; set; } = 10;

    /// &amp;lt;summary&amp;gt;
    /// 启用惯性
    /// &amp;lt;/summary&amp;gt;
    [field: SerializeField]
    public bool EnableInertia { get; set; } = true;

    void Update()
    {
        if (EnableInertia &amp;amp;&amp;amp; !_dragging &amp;amp;&amp;amp; _radianVelocity == 0)
        {
            _radianOffset += _radianVelocity;
            UpdateObjectStatus();

            // 使速度衰减的逻辑 (稍后进行详细的编写)

            // 自动回正
            if (EnableAutoCorrection &amp;amp;&amp;amp; _radianVelocity == 0)
            {
                Select(SelectedIndex);
            }
        }
    }

    void IDragHandler.OnDrag(PointerEventData eventData)
    {
        var radius = Size / 2;

        var mousePosition = Input.mousePosition;
        mousePosition.z = _cameraDistance;
        var newMouseWorldPosition = Camera.main.ScreenToWorldPoint(mousePosition);
        var mouseWorldOffset = newMouseWorldPosition - _lastMouseWorldPosition;

        _lastMouseWorldPosition = newMouseWorldPosition;

        var radianChange = mouseWorldOffset.x / radius;

        // 直接以 radianChange 除以 Time.deltaTime 得到速度
        _radianVelocity = radianChange / Time.deltaTime;
        _radianOffset += radianChange;

        UpdateObjectStatus();
    }

    void IBeginDragHandler.OnBeginDrag(PointerEventData eventData)
    {
        // 其他逻辑
    
        _dragging = true;    // 设置拖拽状态
    }

    void IEndDragHandler.OnEndDrag(PointerEventData eventData)
    {
        _dragging = false;   // 设置拖拽状态

        if (EnableAutoCorrection &amp;amp;&amp;amp; !EnableInertia)
        {
            // 自动回正
            Select(SelectedIndex);
        }
    }

    // 其他逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;速度的衰减, 我们这里通过将速度拆解成两个量来做处理, &quot;方向&quot; 和 &quot;大小&quot;. 然后对大小做减法, 最后再重新拼接.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Carousel : MonoBehavior, IDragHandler, IBeginDragHandler, IEndDragHandler
{
    // 字段与属性

    void Update()
    {
        if (EnableInertia &amp;amp;&amp;amp; !_dragging &amp;amp;&amp;amp; _radianVelocity != 0)
        {
            _radianOffset += _radianVelocity * Time.deltaTime;
            UpdateObjectStatus();

            // 拆解
            var radianVelocitySign = Mathf.Sign(_radianVelocity);
            var radianVelocitySize = Mathf.Abs(_radianVelocity);

            // 减小速度
            radianVelocitySize -= RadianDrag * Time.deltaTime;
            if (radianVelocitySize &amp;lt; 0)   // 速度大小不能为负值
                radianVelocitySize = 0;

            // 重新拼接
            _radianVelocity = radianVelocitySign * radianVelocitySize;

            // 自动回正
            if (EnableAutoCorrection &amp;amp;&amp;amp; _radianVelocity == 0)
            {
                Select(SelectedIndex);
            }
        }
    }

    // 其他逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是, 我们成功为轮转图添加了惯性功能. 最后检查代码, 它们大概是这样的:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Carousel : MonoBehavior, IDragHandler, IBeginDragHandler, IEndDragHandler
{
    float _radianOffset;
    float _radianVelocity;
    
    float _cameraDistance;
    Vector3 _lastMouseWorldPosition;

    public float Size { get; set; } = 5;
    public float RadianDrag { get; set; } = 10;

    public bool EnableInertia { get; set; } = true;
    public bool EnableAutoCorrection { get; set; } = true;
    public float TransitionTime { get; set; } = 0.2f;

    public int SelectedIndex { get; private set; }

    void Start() { ... }
    void Update() { ... }

    private void UpdateObjectStatus() { ... }
    private void CorrectRotationTarget(float origin, ref float target) { ... }
    private float GetRadianFromItemIndex(int index) { ... }

    public void Select(int index) { ... }

    void IDragHandler.OnDrag(PointerEventData eventData) { .. }
    void IBeginDragHandler.OnBeginDrag(PointerEventData eventData) { ... }
    void IEndDragHandler.OnEndDrag(PointerEventData eventData) { ... }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是, 你就可以得到在文章最开始展示的, 完整轮转图的效果了:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/unity_carousel_3d.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h3&gt;点击选择&lt;/h3&gt;
&lt;p&gt;最后, 我们希望为轮转图添加鼠标单击选择的功能, 实现起来很方便, 因为我们已经编写好选择某元素的方法 &lt;code&gt;Select&lt;/code&gt; 了. 那么, 只需要实现 &lt;code&gt;IPointerClickHandler&lt;/code&gt;, 并判断是否点击了某个子元素, 然后调用 &lt;code&gt;Select&lt;/code&gt; 方法即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Carousel : MonoBehavior, IDragHandler, IBeginDragHandler, IEndDragHandler, IPointerClickHandler
{
    // 字段与属性

    /// &amp;lt;summary&amp;gt;
    /// 允许点击选择
    /// &amp;lt;/summary&amp;gt;
    [field: SerializeField]
    public bool AllowClickSelection { get; set; } = false;

    void IPointerClickHandler.OnPointerClick(PointerEventData eventData)
    {
        if (!AllowClickSelection)
            return;

        var childCount = transform.childCount;

        for (int i = 0; i &amp;lt; childCount; i++)
        {
            var renderer = transform.GetChild(i);

            if (eventData.pointerPressRaycast.gameObject == renderer.gameObject)
            {
                Select(i);
                break;
            }
        }
    }

    // 其他逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加完上述代码之后, 我们就得到了可以单击选择的轮转图了.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/unity_carousel_click_selection.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h2&gt;物体生成&lt;/h2&gt;
&lt;p&gt;对于 UI 上的轮转图, 我们以展示 Sprite 为例, 为了方便, 我们不需要像原本那样实现在父物体下放置好物体, 而是根据用户指定的 Sprite, 自动生成游戏对象, 自动挂载所需组件, 设置属性.&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h3&gt;创建物体&lt;/h3&gt;
&lt;p&gt;我们打算让脚本向外暴露出一个 &lt;code&gt;Sprite[]&lt;/code&gt; 供设置要展示的图片, 那么在接下来的逻辑中, 就需要实现:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始化时, 创建所需的游戏对象, 挂载 Image 组件以供显示图片&lt;/li&gt;
&lt;li&gt;当外部对图片数组进行赋值时, 重新更新状态, 使显示是正确的&lt;/li&gt;
&lt;li&gt;由于 Canvas 在 Screen Space - Overlay 模式下不存在近大远小, 所以我们需要提供根据前后顺序, 自动进行缩放的功能&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那么, 我们需要做的添加和变更的有这些:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UICarousel : MonoBehavior, IDragHandler, IBeginDragHandler, IEndDragHandler, IPointerClickHandler
{
    // 其他字段和属性

    [SerializeField]
    Sprite[] _images;

    [SerializeField]
    private bool _scaleImages = true;

    [SerializeField]
    private float _minScale = 0.3f;

    List&amp;lt;UnityEngine.UI.Image&amp;gt; _imageComponents;

    // 懒加载的, 用于获取 RectTransform 的属性
    RectTransform _selfRectTransform;
    RectTransform SelfRectTransform =&amp;gt; _selfRectTransform ??= GetComponent&amp;lt;RectTransform&amp;gt;();

    
    /// &amp;lt;summary&amp;gt;
    /// 要展示的图片
    /// &amp;lt;/summary&amp;gt;
    public Sprite[] Images
    {
        get =&amp;gt; _images; 
        set
        {
            _images = value;
            UpdateImagesStatus();
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 图片尺寸 (用于生成 Image 物体设置大小)
    /// &amp;lt;/summary&amp;gt;
    [field: SerializeField]
    public Vector2 ImageSize { get; set; } = new Vector2(100, 100);
    
    /// &amp;lt;summary&amp;gt;
    ///是否根据图像的前后关系调整图像大小  &amp;lt;br/&amp;gt;
    /// 如果是 Overlay, 不存在近大远小, 则需要开启这个, 但是如果是 WorldSpace 的 Canvas 并且设置了缩放使其在场景内, 则不需要启用这个
    /// &amp;lt;/summary&amp;gt;
    public bool ScaleImages
    {
        get =&amp;gt; _scaleImages;
        set
        {
            _scaleImages = value;
            UpdateImagesStatus();
        }
    }
    
    /// &amp;lt;summary&amp;gt;
    /// 最小缩放比例 (最后方的图像的缩放系数会是这个值)
    /// &amp;lt;/summary&amp;gt;
    public float MinScale
    {
        get =&amp;gt; _minScale; 
        set
        {
            _minScale = value;
            UpdateImagesStatus();
        }
    }

    void Awake()
    {
        // 初始化
        if (_images is null)
            _images = Array.Empty&amp;lt;Sprite&amp;gt;();

        UpdateRenderers();
    }

    /// &amp;lt;summary&amp;gt;
    /// 根据图像创建 Image 对象, 并自动设置属性
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;image&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    private UnityEngine.UI.Image CreateRendererFor(Sprite image) { ... }

    /// &amp;lt;summary&amp;gt;
    /// 更新 Sprite 的渲染器
    /// &amp;lt;/summary&amp;gt;
    private void UpdateRenderers() { ... }

    /// &amp;lt;summary&amp;gt;
    /// 旋转主逻辑
    /// 根据 &quot;旋转偏移量&quot; 设置所有图像的位置, 大小, 以及前后关系
    /// &amp;lt;/summary&amp;gt;
    private void UpdateObjectStatus() { ... }

    // 其他逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据 Sprite 创建 Image 的逻辑:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UICarousel : MonoBehavior, IDragHandler, IBeginDragHandler, IEndDragHandler, IPointerClickHandler
{
    // 字段和属性

    /// &amp;lt;summary&amp;gt;
    /// 根据图像创建 Image 对象, 并自动设置属性
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;image&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    private UnityEngine.UI.Image CreateRendererFor(Sprite image) 
    {
        GameObject gameObject = new(&quot;Image&quot;);
        gameObject.transform.SetParent(transform);

        var rectTransform = gameObject.AddComponent&amp;lt;RectTransform&amp;gt;();
        var renderer = gameObject.AddComponent&amp;lt;UnityEngine.UI.Image&amp;gt;();

        // 设置
        rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, ImageSize.x);
        rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, ImageSize.y);
        renderer.sprite = image;

        return renderer;
    }


    // 其他逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新 Sprite 渲染器的逻辑(删除或创建 Image):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UICarousel : MonoBehavior, IDragHandler, IBeginDragHandler, IEndDragHandler, IPointerClickHandler
{
    // 字段和属性

    /// &amp;lt;summary&amp;gt;
    /// 更新 Sprite 的渲染器
    /// &amp;lt;/summary&amp;gt;
    private void UpdateRenderers()
    {
        if (_imageComponents is null)
            _imageComponents = new List&amp;lt;UnityEngine.UI.Image&amp;gt;(GetComponentsInChildren&amp;lt;UnityEngine.UI.Image&amp;gt;());
    
        if (_imageComponents.Count &amp;lt; _images.Length)
        {
            for (int i = 0; i &amp;lt; _imageComponents.Count; i++)
                _imageComponents[i].sprite = _images[i];
            while (_imageComponents.Count &amp;lt; _images.Length)
                _imageComponents.Add(CreateRendererFor(_images[_imageComponents.Count]));
        }
        else
        {
            for (int i = 0; i &amp;lt; _images.Length; i++)
                _imageComponents[i].sprite = _images[i];
            while (_imageComponents.Count &amp;gt; _images.Length)
            {
                int lastIndex = _imageComponents.Count - 1;
    
                // destroy the last
                Destroy(_imageComponents[lastIndex].gameObject);
    
                // remove the last
                _imageComponents.RemoveAt(lastIndex);
            }
        }
    }


    // 其他逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h3&gt;UI 特有更新&lt;/h3&gt;
&lt;p&gt;更新物体状态的主逻辑(移动图片, 缩放图片, 调整图片层级关系):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UICarousel : MonoBehavior, IDragHandler, IBeginDragHandler, IEndDragHandler, IPointerClickHandler
{
    // 字段和属性

    /// &amp;lt;summary&amp;gt;
    /// 旋转主逻辑
    /// 根据 &quot;旋转偏移量&quot; 设置所有图像的位置, 大小, 以及前后关系
    /// &amp;lt;/summary&amp;gt;
    private void UpdateObjectStatus()
    {
        UpdateRenderers();

        var scaleGap = 1 - MinScale;
        var radianGap = Mathf.PI * 2 / _images.Length;
        var selfSizeDelta = SelfRectTransform.sizeDelta;
        var radius = selfSizeDelta.x / 2;
        var imageCount = _images.Length;
        var halfPi = Mathf.PI / 2;

        var minSin = 1f;
        var selectedImageIndex = -1;

        _radianOffset %= (Mathf.PI * 2);
        for (int i = 0; i &amp;lt; imageCount; i++)
        {
            var scaleShrink = scaleGap * i / (imageCount - 1);
            var renderer = _imageComponents[i];

            float cos = Mathf.Cos(radianGap * i + _radianOffset - halfPi);
            float sin = Mathf.Sin(radianGap * i + _radianOffset - halfPi);
            var x = cos * radius;
            var z = sin * radius;

            if (sin &amp;lt;= minSin)
            {
                selectedImageIndex = i;
                minSin = sin;
            }

            renderer.transform.localPosition = new Vector3(x, 0, z);

            if (ScaleImages)
            {
                var scale = Mathf.Lerp(MinScale, 1, ((-sin) + 1) / 2);
                renderer.transform.localScale = new Vector3(scale, scale, scale);
            }
            else
            {
                renderer.transform.localScale = new Vector3(1, 1, 1);
            }
        }

        // 根据大小, 调整顺序, 因为小的在后面, 所以直接根据大小, 调用调整顺序方法
        foreach (var com in _imageComponents.OrderBy(com =&amp;gt; com.rectTransform.localScale.x))
            com.transform.SetAsLastSibling();

        // 记录当前选择索引以及图像
        SelectedIndex = selectedImageIndex;
    }


    // 其他逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中, 缩放图片中用到了 &lt;code&gt;Mathf.Lerp&lt;/code&gt; 函数, 在 &quot;比例&quot; 参数中, 传入了 &lt;code&gt;(-sin + 1) / 2&lt;/code&gt; 这样的值. 这是因为, 正弦在我们的算法中本来就关系到物体的前后关系, 所以直接对正弦值做处理, 就能得到最终值域为 0 到 1, 可用于 Lerp 的参数了.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;sin&lt;/code&gt;: 从远到近, 值从 1 到 -1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-sin&lt;/code&gt;: 从远到近, 值从 -1 到 1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-sin + 1&lt;/code&gt;: 从远到近, 值从 0 到 2&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(-sin + 1) / 2&lt;/code&gt;: 从远到近, 值从 0 到 1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Mathf.Lerp(MinScale, 1, (-sin + 1) / 2)&lt;/code&gt;: 从远到近, 值从 &lt;code&gt;MinScale&lt;/code&gt; 到 1&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而之所以需要调整顺序, 也是因为在 UI 中, 遮挡关系不由 Z 轴决定, 而是由 UI 层级以及它们在面板中的先后关系决定的.&lt;/p&gt;
&lt;p&gt;另外, 我们也应该删去对鼠标的坐标转换之类的东西, 因为是 UI 层上的拖拽, 所以在 Drag 中只需要使用事件数据的鼠标移动量就可以了:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UICarousel : MonoBehavior, IDragHandler, IBeginDragHandler, IEndDragHandler, IPointerClickHandler
{
    // 字段和属性

    // 删去 _cameraDistance 和 _lastMouseWorldPosition

    /// &amp;lt;summary&amp;gt;
    /// 拖拽时, 计算实际角度偏移量, 并更新图像状态
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;eventData&quot;&amp;gt;&amp;lt;/param&amp;gt;
    void IDragHandler.OnDrag(PointerEventData eventData)
    {
        var selfSizeDelta = SelfRectTransform.sizeDelta;
        float normalizedOffset = eventData.delta.x / (selfSizeDelta.x / 2);
        float radianChange = Mathf.Asin(normalizedOffset % 1);

        _radianOffset += radianChange;
        _radianVelocity = radianChange / Time.deltaTime;

        UpdateObjectStatus();
    }

    void IBeginDragHandler.OnBeginDrag(PointerEventData eventData)
    {
        // 删去了坐标转换逻辑
        
        _dragging = true;
    }


    // 其他逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后, 检查代码, 它们应该是大概这样的:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[RequireComponent(typeof(RectTransform))]
public class UICarousel : MonoBehavior, IDragHandler, IBeginDragHandler, IEndDragHandler, IPointerClickHandler
{
    RectTransform _selfRectTransform;

    float _radianOffset;
    float _radianVelocity;

    bool _dragging;

    [SerializeField]
    Sprite[] _images;

    [SerializeField]
    private bool _scaleImages = true;

    [SerializeField]
    private float _minScale = 0.3f;

    List&amp;lt;UnityEngine.UI.Image&amp;gt; _imageComponents;

    RectTransform SelfRectTransform =&amp;gt; _selfRectTransform ??= GetComponent&amp;lt;RectTransform&amp;gt;();

    public Sprite[] Images { ... }
    public Vector2 ImageSize { get; set; } = new Vector2(100, 100);
    public float RadianDrag { get; set; } = 10;
    public bool ScaleImages { ... }
    public float MinScale { ... }
    
    public bool AllowClickSelection { get; set; } = false;
    public bool EnableInertia { get; set; } = true;
    public bool EnableAutoCorrection { get; set; } = true;
    
    public float TransitionTime { get; set; } = 0.2f;

    public int SelectedIndex { get; private set; }

    void Awake() { ... }
    void Start() { ... }
    void Update() { ... }

    private UnityEngine.UI.Image CreateRendererFor(Sprite image) { ... }
    private void UpdateRenderers() { ... }
    private void UpdateObjectStatus() { ... }
    private void CorrectRotationTarget(float origin, ref float target) { ... }
    private float GetRadianOffsetFromIndex(int index) { ... }
    
    public void Select(int index) { .... }

    void IDragHandler.OnDrag(PointerEventData eventData) { .. }
    void IBeginDragHandler.OnBeginDrag(PointerEventData eventData) { ... }
    void IEndDragHandler.OnEndDrag(PointerEventData eventData) { ... }
    void IPointerClickHandler.OnPointerClick(PointerEventData eventData) { ... }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是, 最终我们就能得到一个可点选, 带惯性, 带自动回正的 2D 轮转图了.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/unity_carousel_2d_click_selection.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h2&gt;其他&lt;/h2&gt;
&lt;p&gt;从效果上来看, 我们编写的轮转图已经完美了, 但仔细看的话, 还是有一些逻辑瑕疵的.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;即使我拖拽轮转图并停止, 旋转速度的值也有可能不为 0
这就导致当开启惯性的时候, 无论如何, 在松开鼠标的时候, 它都会 &quot;动&quot; 一下.&lt;/li&gt;
&lt;li&gt;当鼠标拖拽到屏幕边缘的时候, 鼠标在移动, 但是屏幕上鼠标的位置不改变
这会导致旋转速度被设为 0, 进而导致, 虽然松开了鼠标, 但是因为没有触发惯性, 因而没有执行自动回正的逻辑. 因为启用惯性时, 只有速度从一个非 0 值变成 0 值, 才会执行自动回正.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这上面的问题, 我懒得改了, 改起来的思路的话, 也很简单, 如下:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将速度计算的逻辑扔 Update 里面, 并计算相对上一帧的旋转量, 即可解决问题 1&lt;/li&gt;
&lt;li&gt;在结束拖拽时, 同时判断速度是否为 0, 如果为 0 就意味着惯性逻辑不执行, 那么这时, 直接执行自动回正逻辑即可.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后, 关于本文章中脚本的源代码, 直接在 &lt;a href=&quot;https://github.com/SlimeNull/UnityTests&quot;&gt;github.com/SlimeNull/UnityTests&lt;/a&gt; 中下载即可. 文章中因为涉及到逐步骤以及思路的讲解, 所以代码并不完整. 不过你跟着做的话, 也是能写出来完整的代码的.&lt;/p&gt;
&lt;p&gt;使用方式的话, 3D 轮转图是直接挂父对象上, 然后手动创建子对象. 在运行时会自动设置子对象位置. 2D 轮转图的话, 不需要手动创建子对象, 直接在 &quot;检视器&quot; 中设置要展示的图片, 在启动时便会自动创建对象.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;文章原始链接: https://slimenull.com/p/20240226141713&lt;/p&gt;
</content:encoded></item><item><title>摘要, 加密, 数字签名, 数字证书以及加密通信简要介绍</title><link>https://slimenull.com/posts/20240202221535/</link><guid isPermaLink="true">https://slimenull.com/posts/20240202221535/</guid><description>关于摘要, 非对称与对称加密, 数字签名, 数字证书以及加密通信的简要介绍.</description><pubDate>Fri, 02 Feb 2024 22:15:35 GMT</pubDate><content:encoded>&lt;p&gt;如今 HTTPS 已几乎完全取代 HTTP, 大部分的网络通信也都有使用加密层, 那么对于非对称加密以及数字证书的学习也就不可避免了.&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h2&gt;摘要&lt;/h2&gt;
&lt;p&gt;摘要是一个很简单的概念, 可以类比人类的指纹.&lt;/p&gt;
&lt;p&gt;摘要是由数据计算而来的, 当数据变更的时候, 它的摘要也随之变更. 对比现实就是, 只要是不同的人, 就基本拥有不同的指纹. 而且摘要当然也是很小的, 一般只有几十个字节的大小.&lt;/p&gt;
&lt;p&gt;如果要验证一个人是否确实是某个人, 对比这个人的所有身份信息显然是过于繁琐了. 然而对比指纹就简单快速多了. 在计算机中也是如此.&lt;/p&gt;
&lt;p&gt;当你下载一个文件的时候, 一般还会下载到一个 &quot;摘要文件&quot;, 在下载完毕之后, 你只需要计算一下下载到的文件的摘要, 再和文件提供方提供的摘要文件对比.
如果摘要是相同的, 那么说明我们下载到的文件是无误的. 如果对比有误, 那么则说明我们下载到的文件某些内容不正确, 或者下载的摘要不正确. 这时我们就需要考虑是否需要重新下载了.&lt;/p&gt;
&lt;p&gt;摘要可以视作一种映射, 将大小不一定的数据, 映射到固定大小的数据中. 以 SHA256 为例, 摘要的大小为 256 位, 也就是 32 字节. 由于源数据的可能性是无限的, 而摘要的可能性有限, 所以摘要算法不可避免的会有冲突问题, 即两个不同的数据计算出来的摘要相同.&lt;/p&gt;
&lt;p&gt;这就像现实中的, 三百六十六个人中, 必然有至少两个人的生日是相同的. 所以优秀的摘要算法, 应该使数据计算出来的摘要, 平均的分布, 以减少冲突.&lt;/p&gt;
&lt;p&gt;常见的摘要算法&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h2&gt;加密&lt;/h2&gt;
&lt;p&gt;加密在计算中是保护信息安全的关键. 无论是安全的通信, 或者校验你获得的数据是否安全, 都离不开加密算法.&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h3&gt;对称加密&lt;/h3&gt;
&lt;p&gt;对称加密是最符合直觉的加密方式. 它就像现实中的锁一样, 只能使用唯一一把钥匙打开. 对称加密就是这样的.&lt;/p&gt;
&lt;p&gt;当加密一份数据的时候, 准备好一份密钥, 在解密的时候, 也只能使用相同的密钥进行解密.&lt;/p&gt;
&lt;p&gt;最最简单的加密对称加密方式, 就是 &quot;异或&quot; 加密了. 得益于异或算法的可逆性, 对一个数据执行两次相同的异或操作, 会得到原文.&lt;/p&gt;
&lt;p&gt;下面是使用 C# 实现的简单异或加密方法:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static byte[] XorEncrypt(byte[] data, byte[] key)
{
    byte[] encryptedData = new byte[data.Length];

    for (int i = 0; i &amp;lt; data.Length; i++)
    {
        encryptedData[i] = (byte)(data[i] ^ key[i % key.Length]);
    }

    return encryptedData;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然, 实际已投入使用的加密算法肯定是要比异或加密复杂很多的.&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h3&gt;非对称加密&lt;/h3&gt;
&lt;p&gt;非对称加密的话, 加密与解密的过程, 是有两份密钥的. 我们暂且称之为密钥 A 和密钥 B.&lt;/p&gt;
&lt;p&gt;如果你使用密钥 A 加密, 那么加密后的密文只能通过密钥 B 来解密. 反之, 由密钥 B 加密的密文, 只能使用密钥 A 来解密.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意, 由 A 加密后的密文, 只能由 B 解密. 使用 A 是无法解密由 A 加密的密文的. 反之亦然.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;非对称加密的密钥不像对称加密一样可以随意由用户指定, 由于非对称加密算法基于复杂的数学难题, 这对密钥也只能通过特定算法来生成.&lt;/p&gt;
&lt;p&gt;在实际的使用中, 这对密钥通常一个私有, 一个对外公开, 所以, 这对密钥被称作私钥和公钥.&lt;/p&gt;
&lt;p&gt;举一个实际使用的示例, 假如你与 A 进行通信, A 有一对非对称密钥对, 其中公钥向外公开, 如果 A 发布一些信息, 通过 A 的私钥进行加密, 那么其他人就可以通过公开的密钥进行解密.
如果解密成功, 则说明这段信息确实是由 A 发布的, 而不是其他人. 因为其他人不持有私钥, 他们没有办法伪造信息.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;数字证书&lt;/h2&gt;
&lt;p&gt;数字证书也可以理解为现实中的证书, 可以用来证明一些东西. 一般的, 数字证书中包含以下项:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;持有者信息&lt;/li&gt;
&lt;li&gt;颁布者信息&lt;/li&gt;
&lt;li&gt;颁布时间&lt;/li&gt;
&lt;li&gt;有效期限&lt;/li&gt;
&lt;li&gt;数字签名&lt;/li&gt;
&lt;li&gt;公钥&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;证书中非对称密钥对的私钥由证书持有者拥有, 不会公开.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其中, 数字签名是用来校验当前的数字证书是否有效的.&lt;/p&gt;
&lt;p&gt;数字证书是有继承关系的. 电脑中会内置一些全球公用的 &quot;根证书&quot;, 其他证书则是这些根证书的子证书, 或者说, 这些证书的颁布者就是这些 &quot;根证书机构&quot;.&lt;/p&gt;
&lt;p&gt;当校验其他证书的时候, 会通过这个证书的父证书提供的公钥, 对当前证书数字签名进行校验. 当然, 在使用父证书之前, 也需要对父证书进行校验. 当然, 如果它的父证书是内置在计算机内的, 则视为有效. 因为计算机内置的根证书是始终被视为有效的.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;由此衍生出来一个有意思的现象, 当你使用很旧的操作系统和浏览器时, 访问某些网站, 会提示该网站危险, 但使用新的浏览器就不会. 造成这个现象的原因就是, 该网站的数字证书无法被校验, 旧的操作系统中缺失了一些必要的根证书, 于是缺失的证书以及其下所属所有子证书都无法被校验.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;常见文件格式&lt;/h3&gt;
&lt;p&gt;根据不同的服务器以及服务器的版本, 我们需要用到不同的证书格式, 就市面上主流的服务器来说, 大概有以下格式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;.DER .CER, 文件是二进制格式, 只保存证书, 不保存私钥&lt;/li&gt;
&lt;li&gt;.PEM, 一般是文本格式, 可保存证书, 可保存私钥&lt;/li&gt;
&lt;li&gt;.CRT, 可以是二进制格式, 可以是文本格式, 与 .DER 格式相同, 不保存私钥&lt;/li&gt;
&lt;li&gt;.PFX .P12, 二进制格式, 同时包含证书和私钥, 一般有密码保护&lt;/li&gt;
&lt;li&gt;.JKS, 二进制格式, 同时包含证书和私钥, 一般有密码保护&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中, PEM 是 &quot;Privacy Enhanced Mail&quot; 的意思, 它本身并不是专门用来存储数字证书的, 只是它使用 Base64, 能用来存储数字证书, 所以用的较多. 它的文件内容大概是这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-----BEGIN XXX-----
BASE64 的内容
-----END XXX-----
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它的内容也可以有多端, 所以对于证书的存储, 如果只包含证书本身, 那么它只有一个 &quot;CERTIFICATE&quot; 段, 如果它还包含私钥, 那么它还有一个 &quot;PRIVATE KEY&quot; 段.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;数字签名&lt;/h2&gt;
&lt;p&gt;在上面的非对称加密中, 我们举了简单的例子, 通过非对称加密算法对信息的安全性进行校验. 但实际上, 非对称加密算法的计算过程是极其复杂且消耗性能的. 我们不可能将特别大的一串信息使用非对称加密算法进行加密以及解密, 在信息校验中, 更多使用的是对 &quot;摘要&quot; 进行校验.&lt;/p&gt;
&lt;p&gt;一段包含数字签名的信息大概至少需要包含以下信息:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;信息&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;信息主体&lt;/td&gt;
&lt;td&gt;信息的内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数字证书&lt;/td&gt;
&lt;td&gt;一个可被校验的数字证书, 其中包含用于校验的公钥&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;加密后摘要&lt;/td&gt;
&lt;td&gt;信息主题通过摘要算法计算摘要, 再使用私钥进行加密的结果&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;数字证书是无法伪造的, 在上文中我们已经提到, 而摘要由于是通过只有发布者才持有的私钥进行加密的, 所以同样也无法伪造. 当用户接收到这段信息之后, 先校验数字证书是否有效, 然后再通过数字证书中的公钥对加密后的摘要进行解密, 得到摘要明文, 最后判断信息主题的摘要是否与这段摘要信息匹配就可以了.&lt;/p&gt;
&lt;p&gt;上面的信息中, 信息主体, 数字证书, 加密后摘要, 这三者任何一者产生改变, 都会导致校验失败. 通过这种方式, 就能够同时实现验证信息是否被伪造或变更了.&lt;/p&gt;
&lt;p&gt;而这种通过数字证书和加密后的摘要信息进行校验码的方式, 就叫做数字签名.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;加密通信&lt;/h2&gt;
&lt;p&gt;我们日常中访问的网站, 一般都是加密的. 他们的地址以 &quot;HTTPS&quot; 开头, 其底层就是用到了 &quot;SSL/TLS&quot; 协议, 而这个协议就是用到了非对称加密算法实现加密通信的.&lt;/p&gt;
&lt;p&gt;在前面我们提到, 非对称加密算法的计算是复杂且消耗性能的, 而 HTTPS 进行大量数据传输, 当然也不可能使用非对称加密算法对这些数据进行加密. 事实上, 双方建立通信后, 是通过对称加密算法进行加密信息的, 只需要保证这个对称加密算法所使用的密钥不会被泄露出去就可以.&lt;/p&gt;
&lt;p&gt;也就是说, 非对称加密在加密通信中的作用, 仅仅是对以后要使用的对称密钥进行加密.&lt;/p&gt;
&lt;p&gt;在建立通信时, 服务器会将自己的数字证书发送给客户端, 此时客户端可以对证书进行校验. 而证书中包含非对称密钥对中的 &quot;公钥&quot;, 再然后, 客户端会生成一个随机的密钥, 使用公钥进行加密后, 再发送给服务器.&lt;/p&gt;
&lt;p&gt;由于公钥加密后的数据仅能使用私钥解密, 所以这串密文, 也只能由服务器解密, 于是这个随机生成的对称密钥, 成功安全的送达到服务器了.&lt;/p&gt;
&lt;p&gt;至此, 客户端和服务器就成功协商好了一个对称密钥. 在接下来的通信中, 所有的信息均使用这个隐秘的密钥进行加密.&lt;/p&gt;
</content:encoded></item><item><title>[.NET] 查询当前已安装所有 Win32 与 UWP 应用</title><link>https://slimenull.com/posts/20240117143332/</link><guid isPermaLink="true">https://slimenull.com/posts/20240117143332/</guid><description>通过各种方式, 查询 Windows 中以安装的 Win32 和 UWP 应用, 包括使用 WinAPI, 注册表, COM, 以及 Win SDK</description><pubDate>Wed, 17 Jan 2024 14:33:32 GMT</pubDate><content:encoded>&lt;p&gt;为了获取当前设备用户已安装的所有应用程序,
一般来讲有两种方案. 一种是通过查询 &quot;shell:AppsFolder&quot; 目录下所有项,
一种是从开始菜单中获取所有快捷方式, 然后加上查询所有已安装的 UWP 应用, 最后得到总列表.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如需代码参考, 请看 &lt;a href=&quot;https://github.com/SlimeNull/WindowsAppsQuery&quot;&gt;github.com/SlimeNull/WindowsAppsQuery&lt;/a&gt; 以及 &lt;a href=&quot;https://github.com/OrgEleCho/CurvaLauncher&quot;&gt;github.com/OrgEleCho/CurvaLauncher&lt;/a&gt; 中的 &apos;Run Applictions&apos; 部分.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;查询 Win32 应用&lt;/h2&gt;
&lt;p&gt;查询已安装 Win32 应用程序, 目前有两种常用方式.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查询注册表中 &quot;卸载&quot; 的子键&lt;/li&gt;
&lt;li&gt;搜寻开始菜单目录&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;通过注册表查询&lt;/h3&gt;
&lt;p&gt;注册表中, &lt;code&gt;HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall&lt;/code&gt; 中列举了当前计算机的所有可卸载应用程序
如果是仅安装在当前用户中的应用程序, 则存储在 &lt;code&gt;HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall&lt;/code&gt; 下.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20240201220018.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据微软官方文档 &lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/uninstall-registry-key&quot;&gt;Win32/MSI/卸载注册表键&lt;/a&gt; 中介绍, 上述注册表键的子键中, 属性与 MSI 中的属性有以下对应关系:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;th&gt;Windows Installer 属性&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DisplayName&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/productname&quot;&gt;&lt;strong&gt;ProductName&lt;/strong&gt;&lt;/a&gt; 属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DisplayVersion&lt;/td&gt;
&lt;td&gt;派生自 &lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/productversion&quot;&gt;&lt;strong&gt;ProductVersion&lt;/strong&gt;&lt;/a&gt; 属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publisher&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/manufacturer&quot;&gt;&lt;strong&gt;Manufacturer&lt;/strong&gt;&lt;/a&gt; 属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VersionMinor&lt;/td&gt;
&lt;td&gt;派生自 &lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/productversion&quot;&gt;&lt;strong&gt;ProductVersion&lt;/strong&gt;&lt;/a&gt; 属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VersionMajor&lt;/td&gt;
&lt;td&gt;派生自 &lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/productversion&quot;&gt;&lt;strong&gt;ProductVersion&lt;/strong&gt;&lt;/a&gt; 属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version&lt;/td&gt;
&lt;td&gt;派生自 &lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/productversion&quot;&gt;&lt;strong&gt;ProductVersion&lt;/strong&gt;&lt;/a&gt; 属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HelpLink&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/arphelplink&quot;&gt;&lt;strong&gt;ARPHELPLINK&lt;/strong&gt;&lt;/a&gt; 属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HelpTelephone&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/arphelptelephone&quot;&gt;&lt;strong&gt;ARPHELPTELEPHONE&lt;/strong&gt;&lt;/a&gt; 属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;InstallDate&lt;/td&gt;
&lt;td&gt;此产品最后一次接受服务的时间。 每次从产品应用或移除修补程序或使用 /v &lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/command-line-options&quot;&gt;命令行选项&lt;/a&gt;修复产品时，都会替换此属性的值。 如果产品未接受任何修复或修补，则此属性包含在此计算机上安装该产品的时间。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;InstallLocation&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/arpinstalllocation&quot;&gt;&lt;strong&gt;ARPINSTALLLOCATION&lt;/strong&gt;&lt;/a&gt; 属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;InstallSource&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/sourcedir&quot;&gt;&lt;strong&gt;SourceDir&lt;/strong&gt;&lt;/a&gt; 属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URLInfoAbout&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/arpurlinfoabout&quot;&gt;&lt;strong&gt;ARPURLINFOABOUT&lt;/strong&gt;&lt;/a&gt; 属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URLUpdateInfo&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/arpurlupdateinfo&quot;&gt;&lt;strong&gt;ARPURLUPDATEINFO&lt;/strong&gt;&lt;/a&gt; 属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AuthorizedCDFPrefix&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/arpauthorizedcdfprefix&quot;&gt;&lt;strong&gt;ARPAUTHORIZEDCDFPREFIX&lt;/strong&gt;&lt;/a&gt; 属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comments&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/arpcomments&quot;&gt;&lt;strong&gt;ARPCOMMENTS&lt;/strong&gt;&lt;/a&gt; 属性  &amp;lt;br&amp;gt;提供给“添加或删除程序”控制面板的注释。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contact&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/arpcontact&quot;&gt;&lt;strong&gt;ARPCONTACT&lt;/strong&gt;&lt;/a&gt; 属性  &amp;lt;br&amp;gt;提供给“添加或删除程序”控制面板的联系人。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EstimatedSize&lt;/td&gt;
&lt;td&gt;由 Windows Installer 确定和设置。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/productlanguage&quot;&gt;&lt;strong&gt;ProductLanguage&lt;/strong&gt;&lt;/a&gt; 属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ModifyPath&lt;/td&gt;
&lt;td&gt;由 Windows Installer 确定和设置。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readme&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/arpreadme&quot;&gt;&lt;strong&gt;ARPREADME&lt;/strong&gt;&lt;/a&gt; 属性  &amp;lt;br&amp;gt;提供给“添加或删除程序”控制面板的自述文件。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UninstallString&lt;/td&gt;
&lt;td&gt;由 Windows Installer 确定和设置。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SettingsIdentifier&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/msi/msiarpsettingsidentifier&quot;&gt;&lt;strong&gt;MSIARPSETTINGSIDENTIFIER&lt;/strong&gt;&lt;/a&gt; 属性&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;通过开始菜单目录查询&lt;/h3&gt;
&lt;p&gt;如果是搜寻开始菜单目录, 则是以下路径:&lt;/p&gt;
&lt;p&gt;当前用户的开始菜单程序列表: &lt;code&gt;C:\Users\slime\AppData\Roaming\Microsoft\Windows\Start Menu\Programs&lt;/code&gt;
所有用户公共的开始菜单程序列表: &lt;code&gt;C:\ProgramData\Microsoft\Windows\Start Menu\Programs&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;通过 &lt;code&gt;Environment.GetFolderPath&lt;/code&gt; 方法即可获取上述两个路径. 参数分别是 &lt;code&gt;Programs&lt;/code&gt; 枚举和 &lt;code&gt;CommonPrograms&lt;/code&gt; 枚举&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所有正常安装的 Win32 程序一般都会创建开始菜单项, 所以可以通过这个粗略的获取所有 Win32 应用程序列表.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20240201220106.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后, 所有的应用程序都是通过快捷方式存在于开始菜单中的, 所以你需要有解析快捷方式的方案, 第一种是通过 Shell API, 但这挺麻烦的.
笔者这边推荐的方式是通过 &lt;a href=&quot;https://github.com/securifybv/ShellLink&quot;&gt;Securify.ShellLink&lt;/a&gt; 这个库来解析.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 从文件读取快捷方式信息
var shortcut = Shortcut.ReadFromFile(@&quot;C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Accessories\Paint.lnk&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是, 快捷方式的结构是很复杂的, 你想要的 &quot;目标路径&quot; 可能在快捷方式中不一定哪个属性中, 可以参考以下的 Fallback 顺序:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ExtraData.EnvironmentVariableDataBlock.TargetUnicode&lt;/li&gt;
&lt;li&gt;LinkTargetIDList.Path&lt;/li&gt;
&lt;li&gt;LinkTargetIDList.DisplayName&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在获取这些属性时, 记得对属性进行判空, 以免遇到 NullReferenceException&lt;/p&gt;
&lt;p&gt;如果要获取其他信息, 诸如命令行参数, 工作目录, 图标此类, 直接获取快捷方式的对应属性即可.
至于以管理员权限身份运行, 这个直接判断 LinkFlags 是否有 &lt;code&gt;RunAsUser&lt;/code&gt; 这个标志就可以了.&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h2&gt;查询 UWP 应用&lt;/h2&gt;
&lt;p&gt;查询已安装的 UWP 应用程序, 有你能轻易在网上找到的三种查询方式, 以及笔者自己找到的第四种方式.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 Windows SDK 中的 &lt;code&gt;PackageManager&lt;/code&gt; 类进行最正规的查询&lt;/li&gt;
&lt;li&gt;使用防火墙的 API, 通过查询 UWP 容器列表, 实现查找所有 UWP 应用&lt;/li&gt;
&lt;li&gt;调用 PowerShell, 执行 &lt;code&gt;Get-AppxPackage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;查注册表&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;通过 Windows SDK 查询&lt;/h3&gt;
&lt;p&gt;第一种方案, &lt;code&gt;PackageManager&lt;/code&gt; 并不是你可以直接使用到的类, 而是需要使用 Windows SDK 才可以用的. 对项目的配置方式也简单.&lt;/p&gt;
&lt;p&gt;如果你是 .NET CoreCLR 的项目, 例如 .NET8 的, 那么直接右键项目, 打开属性, 将目标 OS 改为 Windows, 目标 OS 版本改为你的目标系统版本, 支持的 OS 版本则是最低 OS 版本.
这里建议 1904 什么的, 例如 10.0.19041.0, 选择更低的版本, 可以支持更多系统.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20240201215116.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果你是 .NET Framework 的项目, 那么你需要知道你的 Windows SDK 安装在哪, 在它下面会有一个 Windows.winmd 动态链接库, 它大概在一个叫 &apos;UnionMetadata&apos; 的目录下, 右键你的项目, 添加引用, 然后把这个 winmd 库添加进去即可.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20240201215808.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;配置完之后, 你就可以使用 &apos;PackageManager&apos; 进行查询了, 查询当前用户的所有已安装的包不需要什么特殊权限, 代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System.Security.Principal;
using Windows.Management.Deployment;

PackageManager packageManager = new PackageManager();
var packages = packageManager.FindPackagesForUser(WindowsIdentity.GetCurrent().User!.Value);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;通过防火墙 API 查询&lt;/h3&gt;
&lt;p&gt;第二种方案, 通过 WinAPI NetworkIsolationEnumAppContainers 枚举所有的应用程序容器, 进而实现查询 UWP 应用程序.&lt;/p&gt;
&lt;p&gt;非托管函数声明以及结构体定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static class NativeDll
{
    /// &amp;lt;summary&amp;gt;
    /// 加载资源文本
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;pszSource&quot;&amp;gt;资源标识符&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;pszOutBuf&quot;&amp;gt;输出的缓冲区&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;cchOutBuf&quot;&amp;gt;缓冲区大小&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;ppvReserved&quot;&amp;gt;保留, 固定0&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    [DllImport(&quot;shlwapi.dll&quot;, BestFitMapping = false, CharSet = CharSet.Unicode, ExactSpelling = true, ThrowOnUnmappableChar = true)]
    public static extern unsafe uint SHLoadIndirectString(string pszSource, ref char pszOutBuf, int cchOutBuf, IntPtr ppvReserved);

    [DllImport(&quot;FirewallAPI.dll&quot;)]
    public static extern uint NetworkIsolationEnumAppContainers(uint Flags, out uint pdwCntPublicACs, out IntPtr ppPublicACs);

    [DllImport(&quot;FirewallAPI.dll&quot;)]
    public static extern void NetworkIsolationFreeAppContainers(IntPtr pACs);

    public static string? GetIndirectString(string str)
    {
        Span&amp;lt;char&amp;gt; buffer = stackalloc char[4096];
        if (SHLoadIndirectString(str, ref buffer[0], buffer.Length, 0) != 0)
        {
            return null;
        }
        return new string(buffer.TrimEnd(&apos;\0&apos;));
    }
}

internal struct INET_FIREWALL_APP_CONTAINER
{
    internal IntPtr appContainerSid;

    internal IntPtr userSid;

    [MarshalAs(UnmanagedType.LPWStr)]
    internal string appContainerName;

    [MarshalAs(UnmanagedType.LPWStr)]
    internal string displayName;

    [MarshalAs(UnmanagedType.LPWStr)]
    internal string description;

    internal INET_FIREWALL_AC_CAPABILITIES capabilities;

    internal INET_FIREWALL_AC_BINARIES binaries;

    [MarshalAs(UnmanagedType.LPWStr)]
    internal string workingDirectory;

    [MarshalAs(UnmanagedType.LPWStr)]
    internal string packageFullName;
}

internal struct INET_FIREWALL_AC_CAPABILITIES
{
    public uint count;

    public IntPtr capabilities;
}

internal struct INET_FIREWALL_AC_BINARIES
{
    public uint count;

    public IntPtr binaries;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单封装一下应用容器类:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System.Runtime.InteropServices;

internal record class AppContainer
{
    public string DisplayName { get; set; }
    public string Description { get; set; }
    public string PackageFullName { get; set; }
    public string[] Binaries { get; set; } = [];
    public string WorkingDirectory { get; set; }

    public AppContainer(INET_FIREWALL_APP_CONTAINER info)
    {
        PackageFullName = info.packageFullName;
        WorkingDirectory = info.workingDirectory;
        uint HRESULT = 0;

        Span&amp;lt;char&amp;gt; buffer = stackalloc char[4096];
        HRESULT = NativeDll.SHLoadIndirectString(info.displayName, ref buffer[0], buffer.Length, 0);
        if (HRESULT == 0)
            DisplayName = new string(buffer.TrimEnd(&apos;\0&apos;));
        else
            DisplayName = info.displayName;
        buffer.Clear();

        if (NativeDll.SHLoadIndirectString(info.description, ref buffer[0], buffer.Length, 0) == 0)
            Description = new string(buffer.TrimEnd(&apos;\0&apos;));
        else
            Description = info.description;
        buffer.Clear();

        INET_FIREWALL_AC_BINARIES inet_FIREWALL_AC_BINARIES = info.binaries;
        if (inet_FIREWALL_AC_BINARIES.count &amp;gt; 0 &amp;amp;&amp;amp; inet_FIREWALL_AC_BINARIES.binaries != 0)
        {
            Binaries = new string[inet_FIREWALL_AC_BINARIES.count];
            for (int i = 0; i &amp;lt; inet_FIREWALL_AC_BINARIES.count; i++)
            {
                var str = Marshal.PtrToStringUni(Marshal.ReadIntPtr(inet_FIREWALL_AC_BINARIES.binaries + nint.Size * i));
                ;
                if (str?.StartsWith(@&quot;\\?\&quot;) == true)
                {
                    str = str[4..];
                }

                if (str != null)
                {
                    Binaries[i] = str;
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var enumResult = NativeDll.NetworkIsolationEnumAppContainers(0, out var num, out var ptr);
if (enumResult != 0)
    throw new Win32Exception((int)enumResult, Marshal.GetLastPInvokeErrorMessage());

List&amp;lt;AppContainer&amp;gt; appContainers = new((int)num);
for (int i = 0; i &amp;lt; num; i++)
{
    var info = Marshal.PtrToStructure&amp;lt;INET_FIREWALL_APP_CONTAINER&amp;gt;(ptr + Marshal.SizeOf&amp;lt;INET_FIREWALL_APP_CONTAINER&amp;gt;() * i);
    appContainers.Add(new AppContainer(info));
}

NativeDll.NetworkIsolationFreeAppContainers(ptr);

foreach (var appContainer in appContainers)
    Console.WriteLine(appContainer);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这种方式调用也有一点缺点, 你无法获取应用程序的图标.
以上代码来自于 &lt;a href=&quot;https://github.com/ilyfairy&quot;&gt;Ilyfairy&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;通过注册表查询&lt;/h3&gt;
&lt;p&gt;第三种方案, 在注册表的 &quot;HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\Repository\Packages&quot; 下, 你可以看到所有已安装包的部分信息&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20240201232429.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中 &quot;PackageId&quot; 是该包的 ID, 而 &quot;PackageRootFolder&quot; 直接给出了包的根文件夹, 而这个文件夹中, 我们可以找到包的清单文件 &quot;AppxManifest.xml&quot;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20240201232706.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;清单中列出了所有该包内所有应用程序的信息, 包括应用名称, LOGO, 以及该应用是否对用户可见(展示在开始菜单中)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20240201233019.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;部分应用程序, 在 Manifest 中, 应用程序的显示名称是直接以文本形式存在的, 但是也有一些是通过资源路径的形式存在的. 我们需要加载这个文本资源, 来获取其实际文本.&lt;/p&gt;
&lt;p&gt;这里再次用到了 &quot;SHLoadIndirectString&quot; 这个 API:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[DllImport(&quot;shlwapi.dll&quot;, BestFitMapping = false, CharSet = CharSet.Unicode, ExactSpelling = true, ThrowOnUnmappableChar = true)]
static extern unsafe uint SHLoadIndirectString(string pszSource, ref char pszOutBuf, int cchOutBuf, IntPtr ppvReserved);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过一个键获取其对应所有应用程序的逻辑如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 类型定义
public abstract record class AppInfo
{
    private string? _queryRoot;

    public string Name { get; set; } = string.Empty;

    public string QueryRoot { get =&amp;gt; _queryRoot ?? Name; set =&amp;gt; _queryRoot = value; }

    public string[]? AlterQueryRoots { get; set; }
}

public record class UwpAppInfo : AppInfo
{
    public string PackageId { get; set; } = string.Empty;
    public string FamilyID { get; set; } = string.Empty;
    public string PackageRootFolder { get; set; } = string.Empty;
    public string AppxManifestPath =&amp;gt; Path.Combine(PackageRootFolder, &quot;AppxManifest.xml&quot;);

    public string ApplicationId { get; set; } = string.Empty;
    public UwpAppLogo[] AppLogos { get; set; } = Array.Empty&amp;lt;UwpAppLogo&amp;gt;();

    public required string OriginRegistryKeyName { get; set; } = string.Empty;
    
    public record struct UwpAppLogo(int Size, string Path);
}

// 方法定义
private IEnumerable&amp;lt;UwpAppInfo&amp;gt; GetUwpApps(RegistryKey subKey)
{
    if (subKey.GetValue(&quot;PackageID&quot;) is not string packageId)
        yield break;

    string packageFamilyId = Regex.Replace(packageId, &quot;_.*__&quot;, &quot;_&quot;);

    if (subKey.GetValue(&quot;PackageRootFolder&quot;) is not string packageRootFolder)
        yield break;

    string packageManifestPath = Path.Combine(packageRootFolder, &quot;AppxManifest.xml&quot;);

    if (!System.IO.File.Exists(packageManifestPath))
        yield break;

    var appxManifestContent = File.ReadAllText(packageManifestPath);

    XmlDocument xml = new();
    xml.LoadXml(appxManifestContent);

    XmlNamespaceManager nsManager = new XmlNamespaceManager(xml.NameTable);//这一步实例化一个xml命名空间管理器
    nsManager.AddNamespace(&quot;ns&quot;, &quot;http://schemas.microsoft.com/appx/manifest/foundation/windows10&quot;);
    nsManager.AddNamespace(&quot;uap&quot;, &quot;http://schemas.microsoft.com/appx/manifest/uap/windows10&quot;);

    var idNode = xml.SelectSingleNode(&quot;/ns:Package/ns:Identity&quot;, nsManager);
    var appNodes = xml.SelectNodes(&quot;/ns:Package/ns:Applications/ns:Application&quot;, nsManager);

    if (appNodes == null)
        yield break;

    foreach (XmlNode appNode in appNodes)
    {
        var visualElementsNode = appNode.SelectSingleNode(&quot;uap:VisualElements&quot;, nsManager);

        if (visualElementsNode == null)
            continue;

        if (visualElementsNode?.Attributes?[&quot;AppListEntry&quot;]?.Value is string appListEntry &amp;amp;&amp;amp;
            appListEntry.Equals(&quot;none&quot;, StringComparison.OrdinalIgnoreCase))
            continue;

        if (visualElementsNode?.Attributes?[&quot;DisplayName&quot;]?.Value is not string displayName)
            continue;

        UwpAppInfo info = new()
        {
            OriginRegistryKeyName = Path.GetFileName(subKey.Name)
        };

        info.PackageId = packageId;
        info.FamilyID = packageFamilyId;
        info.PackageRootFolder = packageRootFolder;

        var logoNode = xml.SelectSingleNode(&quot;/ns:Package/ns:Properties/ns:Logo&quot;, nsManager);

        if (appNode == null ||
            appNode.Attributes?[&quot;Id&quot;]?.Value is not string appId)
            continue;

        info.ApplicationId = appId;

        if (displayName.StartsWith(resourcePrefix))
        {
            string resourcePath = displayName.Substring(resourcePrefix.Length);

            // 非绝对路径, 且能够找到 Identity 节点
            if (!resourcePath.StartsWith(&quot;//&quot;) &amp;amp;&amp;amp;
                idNode?.Attributes?[&quot;Name&quot;]?.Value is string id)
            {
                // 不是引用其他资源, 则添加 &apos;Resource&apos; 前缀
                if (!resourcePath.Contains(&quot;Resources&quot;))
                    resourcePath = $&quot;Resources/{resourcePath}&quot;;

                // 转为绝对资源
                resourcePath = $&quot;//{id}/{resourcePath}&quot;;
            }

            string resourceStr = $&quot;@{{{packageId}?ms-resource:{resourcePath}}}&quot;;
            uint errCode = SHLoadIndirectString(resourceStr, ref displayNameBuffer[0], displayNameBuffer.Length, 0);

            int endIndex = Array.IndexOf(displayNameBuffer, &apos;\0&apos;);
            displayName = new string(displayNameBuffer, 0, endIndex);

            if (errCode != 0)
                continue;
        }

        if (string.IsNullOrWhiteSpace(displayName))
            continue;

        info.Name = displayName;

        string? logoPath = logoNode?.InnerText;

        if (logoPath is not null)
        {
            var logoFullPath = Path.Combine(info.PackageRootFolder, logoPath);

            var logoFileName = Path.GetFileNameWithoutExtension(logoPath);
            var logoExtension = Path.GetExtension(logoPath);
            var logoFilesDir = Path.GetDirectoryName(logoFullPath) ?? &quot;.&quot;;
            var logoFilesPattern = $&quot;{logoFileName}*{logoExtension}&quot;;

            if (Directory.Exists(logoFilesDir))
            {
                var regex = new Regex($@&quot;{Regex.Escape(logoFileName)}(\.scale-(?&amp;lt;scale&amp;gt;\d+))?{Regex.Escape(logoExtension)}&quot;);

                List&amp;lt;UwpAppInfo.UwpAppLogo&amp;gt; logos = new();
                foreach (var searchedLogoFile in Directory.EnumerateFiles(logoFilesDir, logoFilesPattern))
                {
                    var searchedLogoFileName = Path.GetFileName(searchedLogoFile);
                    var match = regex.Match(searchedLogoFileName);

                    if (!match.Success)
                        continue;

                    int scale = 1;
                    if (int.TryParse(match.Groups[&quot;scale&quot;].Value, out var parsedScale))
                        scale = parsedScale;

                    logos.Add(new UwpAppInfo.UwpAppLogo(44 * scale, searchedLogoFile));
                }

                info.AppLogos = logos.ToArray();
            }
        }

        yield return info;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;上述代码来自于 &lt;a href=&quot;https://github.com/OrgEleCho/CurvaLauncher&quot;&gt;github.com/OrgEleCho/CurvaLauncher&lt;/a&gt; 中 &apos;Run Application&apos; 的 UWP 应用索引逻辑&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;h2&gt;查询 AppsFolder&lt;/h2&gt;
&lt;p&gt;使用 &apos;运行&apos; 对话框, 打开 &lt;code&gt;shell:AppsFolder&lt;/code&gt;, 你可以打开一个同时包含 Win32 和 UWP 应用的文件夹,
但这个文件夹实际上是不存在的, 它是 Shell 抽象出来的, 本质上是开始菜单的 &lt;code&gt;CommonPrograms&lt;/code&gt; 和 &lt;code&gt;Programs&lt;/code&gt; 目录, 以及所有 UWP 应用程序加起来, 构成的一个虚拟目录.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20240201215927.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不过, 我们仍然可以通过编程手段来遍历这个 &quot;目录&quot;, 获取所有已安装的应用程序. 下面是通过 COM 查询 AppsFolder 的 C++ 代码.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;Windows.h&amp;gt;
#include &amp;lt;ShlObj.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;io.h&amp;gt;
#include &amp;lt;iostream&amp;gt;
using namespace std;

int main() {
    if (_setmode(_fileno(stdout), _O_U16TEXT) == -1) {
        wcout &amp;lt;&amp;lt; L&quot;Failed to set stdout to UTF-16&quot; &amp;lt;&amp;lt; endl;
        return -1;
    }

    HRESULT result;

    result = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
    if (FAILED(result)) {
        wcout &amp;lt;&amp;lt; L&quot;CoInitializeEx failed: &quot; &amp;lt;&amp;lt; hex &amp;lt;&amp;lt; result &amp;lt;&amp;lt; endl;
        return -1;
    }

    IShellItem* appsFolder;
    result = SHGetKnownFolderItem(FOLDERID_AppsFolder, KF_FLAG_DEFAULT, nullptr, IID_PPV_ARGS(&amp;amp;appsFolder));
    if (FAILED(result)) {
        wcout &amp;lt;&amp;lt; L&quot;SHGetKnownFolderItem failed: &quot; &amp;lt;&amp;lt; hex &amp;lt;&amp;lt; result &amp;lt;&amp;lt; endl;
        return -1;
    }

    IEnumShellItems* appsFolderEnum;
    result = appsFolder-&amp;gt;BindToHandler(nullptr, BHID_EnumItems, IID_PPV_ARGS(&amp;amp;appsFolderEnum));
    if (FAILED(result)) {
        wcout &amp;lt;&amp;lt; L&quot;BindToHandler failed: &quot; &amp;lt;&amp;lt; hex &amp;lt;&amp;lt; result &amp;lt;&amp;lt; endl;
        return -1;
    }

    for (IShellItem* app; appsFolderEnum-&amp;gt;Next(1, &amp;amp;app, nullptr) == S_OK; app-&amp;gt;Release()) {
        // 获取应用程序的名称
        PWSTR name;
        result = app-&amp;gt;GetDisplayName(SIGDN_NORMALDISPLAY, &amp;amp;name);
        if (FAILED(result)) {
            wcout &amp;lt;&amp;lt; L&quot;GetDisplayName failed: &quot; &amp;lt;&amp;lt; hex &amp;lt;&amp;lt; result &amp;lt;&amp;lt; endl;
            return -1;
        }
        wcout &amp;lt;&amp;lt; name &amp;lt;&amp;lt; L&quot; -&amp;gt; &quot;;
        CoTaskMemFree(name);

        // 获取应用程序的路径
        PWSTR path;
        result = app-&amp;gt;GetDisplayName(SIGDN_PARENTRELATIVEPARSING, &amp;amp;path);
        if (FAILED(result)) {
            wcout &amp;lt;&amp;lt; L&quot;GetDisplayName failed: &quot; &amp;lt;&amp;lt; hex &amp;lt;&amp;lt; result &amp;lt;&amp;lt; endl;
            return -1;
        }
        wcout &amp;lt;&amp;lt; path &amp;lt;&amp;lt; L&quot; = &quot;;
        CoTaskMemFree(path);

        // 获取应用程序的图标
        IShellItemImageFactory* imageFactory;
        result = app-&amp;gt;QueryInterface(IID_PPV_ARGS(&amp;amp;imageFactory));
        if (FAILED(result)) {
            wcout &amp;lt;&amp;lt; L&quot;QueryInterface failed: &quot; &amp;lt;&amp;lt; hex &amp;lt;&amp;lt; result &amp;lt;&amp;lt; endl;
            return -1;
        }
        HBITMAP bitmap;
        result = imageFactory-&amp;gt;GetImage(SIZE { 64, 64 }, SIIGBF_ICONONLY, &amp;amp;bitmap);
        if (FAILED(result)) {
            wcout &amp;lt;&amp;lt; L&quot;GetImage failed: &quot; &amp;lt;&amp;lt; hex &amp;lt;&amp;lt; result &amp;lt;&amp;lt; endl;
            return -1;
        }
        wcout &amp;lt;&amp;lt; bitmap &amp;lt;&amp;lt; endl;
        DeleteObject(bitmap);
        imageFactory-&amp;gt;Release();
    }

    appsFolderEnum-&amp;gt;Release();
    appsFolder-&amp;gt;Release();
    CoUninitialize();

    // 原神, 启动!
    // ShellExecuteW(nullptr, L&quot;open&quot;, L&quot;explorer&quot;, L&quot;shell:AppsFolder\\Kingsoft.Office.ET&quot;, nullptr, SW_NORMAL);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[Unity] 基于迭代器的协程底层原理详解</title><link>https://slimenull.com/posts/20231213153716/</link><guid isPermaLink="true">https://slimenull.com/posts/20231213153716/</guid><description>Unity 协程的本质无非就是在合适的实际执行迭代器的MoveNext方法. 对当前正在等待的对象进行条件判断, 如果满足条件, 则MoveNext, 否则就不执行.</description><pubDate>Wed, 13 Dec 2023 15:37:16 GMT</pubDate><content:encoded>&lt;p&gt;Unity 是单线程设计的游戏引擎, 所有对于 Unity 的调用都应该在主线程执行. 倘若我们要实现另外再执行一个任务, 该怎么做呢? 答案就是协程.&lt;/p&gt;
&lt;p&gt;协程本质上是基于 C# yield 迭代器的, 使用 yield 语法生成的返回迭代器的方法, 其内部的逻辑执行, 是 &quot;懒&quot; 的, 只有在调用 &lt;code&gt;MoveNext&lt;/code&gt; 的时候, 才会继续执行下一步逻辑.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;Unity 生命周期&lt;/h2&gt;
&lt;p&gt;我们知道, Unity 在运行的时候, 本质上是有一个主循环, 不断的调用所有游戏对象的各个事件函数, 诸如 &lt;code&gt;Update&lt;/code&gt;, &lt;code&gt;LateUpdate&lt;/code&gt;, &lt;code&gt;FixedUpdate&lt;/code&gt;, 以及在这个主循环中, 进行游戏主逻辑的更新. 其中协程的处理也是在这里完成的.&lt;/p&gt;
&lt;p&gt;Unity 在每一个游戏对象中都维护一个协程的列表, 该对象启动一个协程的时候, 该协程的迭代器就会被放置到 &quot;正在执行的协程&quot; 列表中. Unity 每一帧都会对他们进行判断, 是否应该调用 &lt;code&gt;MoveNext&lt;/code&gt; 方法.&lt;/p&gt;
&lt;p&gt;又因为迭代器有 &quot;懒执行&quot; 的特性, 所以就能够实现, 等待某些操作结束, 然后执行下一段逻辑.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于迭代器懒执行, 参考: &lt;a href=&quot;/posts/20231208192209/&quot;&gt;[C#] 基于 yield 语句的迭代器逻辑懒执行&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;仿写协程&lt;/h2&gt;
&lt;p&gt;光是口述, 肯定是无法讲明白协程原理的, 下面将使用代码简单实现一个协程.&lt;/p&gt;
&lt;p&gt;我们游戏引擎将有以下文件:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GameEngine&lt;/code&gt; : 游戏引擎, 存储所有的游戏对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GameObject&lt;/code&gt; : 表示一个游戏对象, 将会存储其正在运行的协程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GameObjectStates&lt;/code&gt; : 表示一个游戏对象的状态, 例如它是否已经启动, 是否被销毁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Coroutine&lt;/code&gt; : 表示一个正在运行的协程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WaitForSeconds&lt;/code&gt; : 表示一个要等待的对象, 它将使协程暂停执行指定秒数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Program&lt;/code&gt; : 游戏引擎的主循环逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以及用户的逻辑:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MyGameObject&lt;/code&gt; : 用户自定义的游戏对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;p&gt;首先创建一个 &lt;code&gt;GameEngine&lt;/code&gt; 类, 它将容纳当前创建好的所有游戏对象.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class GameEngine
{
    // 私有构造函数, 使外部无法直接被调用
    private GameEngine()
    { }

    // 单例模式
    public static GameEngine Current { get; } = new();

    // 所有的游戏对象
    internal List&amp;lt;GameObject&amp;gt; _allGameObjects = new();

    // 通过 ReadOnlyList 向外暴露所有游戏对象
    public IReadOnlyList&amp;lt;GameObject&amp;gt; AllGameObjects =&amp;gt; _allGameObjects;
    public int FrameNumber { get; internal set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建一个 &lt;code&gt;WaitForSeconds&lt;/code&gt; 类, 它和 Unity 中的 &lt;code&gt;WaitForSeconds&lt;/code&gt; 类一样, 用于在写成中通过 yield 返回实现等待指定时间.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class WaitForSeconds
{
    public WaitForSeconds(float seconds)
    {
        Seconds = seconds;
    }

    public float Seconds { get; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来, 创建一个 &lt;code&gt;Coroutine&lt;/code&gt; 类, 它表示一个正在运行的协程, 构造时, 传入协程要执行的逻辑, 也就是一个 &lt;code&gt;IEnumerator&lt;/code&gt;. 其中, 包含一个 &quot;当前的等待对象&quot; 以及 &quot;当前等待对象相关联的某些参数数据&quot;. 它的 &lt;code&gt;Update&lt;/code&gt; 方法会在游戏主循环中不断被调用.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System.Collections;

public class Coroutine
{
    public Coroutine(IEnumerator enumerator)
    {
        Enumerator = enumerator;
    }

    public IEnumerator Enumerator { get; }

    // 当前等待对象
    object? currentWaitable;

    // 与当前等待对象相关联的参数信息
    object? currentWaitableParameter;

    public bool IsCompleted { get; set; }

    internal void Update()
    {
        // 如果当前协程已经结束, 就不再进行任何操作
        if (IsCompleted)
            return;

        // 如果当前没有要等待的对象
        if (currentWaitable == null)
        {
            // 执行迭代器的 &quot;MoveNext&quot;
            if (!Enumerator.MoveNext())
            {
                // 如果迭代器返回了 false, 也就是迭代器没有下一个数据了
                // 则表示当前协程已经运行结束, 做上标记, 然后返回
                IsCompleted = true;
                return;
            }

            // 如果当前等待对象是 &quot;等待指定秒&quot;
            if (Enumerator.Current is WaitForSeconds waitForSeconds)
            {
                // 保存当前等待对象
                currentWaitable = waitForSeconds;

                // 将当前时间作为参数存起来
                currentWaitableParameter = DateTime.Now;
            }
            else if (Enumerator.Current is Coroutine coroutine)
            {
                // 如果当前等待对象是另一个协程
                // 保存当前等待对象
                currentWaitable = coroutine;
            }
        }
        else   // 否则, 也就是当当前等待对象不为空时
        {
            // 如果当前等待对象是 &quot;等待指定秒&quot;
            if (currentWaitable is WaitForSeconds waitForSeconds)
            {
                DateTime startTime = (DateTime)currentWaitableParameter!;
                
                // 判断是否等待结束
                if ((DateTime.Now - startTime).TotalSeconds &amp;gt;= waitForSeconds.Seconds)
                {
                    // 如果等待结束, 那么就将当前等待对象置空
                    // 这样下一次被调用 Update 时, 就会通过调用迭代器 MoveNext
                    // 执行协程的下一段逻辑, 并且获取下一个等待对象
                    currentWaitable = null;
                }
            }
            else if (currentWaitable is Coroutine coroutine)
            {
                // 如果等待对象是协程, 并且对应协程已经执行完毕
                if (coroutine.IsCompleted)
                {
                    // 将当前等待对象置空
                    currentWaitable = null;
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写一个 &lt;code&gt;GameObjectStates&lt;/code&gt;  来表示一个游戏对象的状态, 例如是否启动了, 是否被销毁了什么的.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;internal class GameObjectStates
{
    // 对应游戏对象
    public GameObject Target { get; }

    // 是否已经启动
    public bool Started { get; set; }

    // 是否已经被销毁
    public bool Destroyed { get; set; }

    public GameObjectStates(GameObject target)
    {
        Target = target;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面, 编写一个 &lt;code&gt;GameObject&lt;/code&gt;, 因为协程是运行在游戏对象中的, 所以游戏对象会有一个容器来承载当前游戏对象正在运行的协程. 当然, 它也有 &lt;code&gt;Start&lt;/code&gt; 和 &lt;code&gt;Update&lt;/code&gt; 两个虚方法, 会被游戏的主逻辑调用.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System.Collections;

public class GameObject
{
    // 当前游戏对象的状态
    internal GameObjectStates States { get; }

    // 所有正在运行的协程
    List&amp;lt;Coroutine&amp;gt; coroutines = new();

    // 即将开始运行的协程
    List&amp;lt;Coroutine&amp;gt; coroutinesToAdd = new();

    // 将要被删除的协程
    List&amp;lt;Coroutine&amp;gt; coroutinesToRemove = new();

    public GameObject()
    {
        // 初始化状态
        States = new(this);

        // 将当前游戏对象添加到游戏引擎
        GameEngine.Current._allGameObjects.Add(this);
    }

    // 由游戏引擎调用的 Start 和 Update
    public virtual void Start() { }
    public virtual void Update() { }

    // 由游戏引擎调用的, 更新所有协程的逻辑
    internal void UpdateCoroutines()
    {
        // 将需要添加的所有协程添加到当前正在运行的协程中
        foreach (var coroutine in coroutinesToAdd)
        {
            coroutines.Add(coroutine);
        }

        coroutinesToAdd.Clear();

        // 更新当前所有协程
        foreach (var coroutine in coroutines)
        {
            coroutine.Update();

            // 如果当前协程已经执行完毕, 则将其添加到 &quot;删除列表&quot; 中
            if (coroutine.IsCompleted)
            {
                coroutinesToRemove.Add(coroutine);
            }
        }

        // 将准备删除的所有协程从当前运行的协程列表中删除
        foreach (var coroutine in coroutinesToRemove)
        {
            coroutines.Remove(coroutine);
        }

        coroutinesToRemove.Clear();
    }

    // 开启一个协程
    public Coroutine StartCoroutine(IEnumerator enumerator)
    {
        Coroutine coroutine = new(enumerator);
        coroutinesToAdd.Add(coroutine);

        return coroutine;
    }

    // 停止一个协程
    public void StopCoroutine(Coroutine coroutine)
    {
        coroutinesToRemove.Add(coroutine);
    }

    // 停止一个协程
    public void StopCoroutine(IEnumerator enumerator)
    {
        int index = coroutines.FindIndex(c =&amp;gt; c.Enumerator == enumerator);
        if (index != -1)
            coroutinesToRemove.Add(coroutines[index]);
    }

    // 销毁当前游戏对象
    public void DestroySelf()
    {
        States.Destroyed = true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自定义一个游戏对象 &lt;code&gt;MyGameObject&lt;/code&gt;, 它在 &lt;code&gt;Start&lt;/code&gt; 时启动一个协程.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System.Collections;

class MyGameObject : GameObject 
{
    public override void Start()
    {
        base.Start();
        StartCoroutine(MyCoroutineLogic());
    }


    IEnumerator MyCoroutineLogic()
    {
        System.Console.WriteLine(&quot;Logic out&quot;);
        yield return StartCoroutine(MyCoroutineLogicInner());
        yield return new WaitForSeconds(3);
        System.Console.WriteLine(&quot;Logic out end&quot;);
    }

    IEnumerator MyCoroutineLogicInner() 
    {
        for (int i = 0; i &amp;lt; 5; i++)
        {
            yield return new WaitForSeconds(1);
            Console.WriteLine($&quot;Coroutine inner {i}&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序主逻辑, 创建自定义的游戏对象, 并执行主循环:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 创建自定义的游戏对象
new MyGameObject();

// 要被销毁的游戏对象
List&amp;lt;GameObject&amp;gt; objectsToDestroy = new();

while (true)
{
    // 对所有游戏对象执行 Start
    foreach (var obj in GameEngine.Current.AllGameObjects)
    {
        if (!obj.States.Started)
        {
            obj.Start();
            obj.States.Started = true;
        }
    }

    // 调用所有游戏对象的 Update
    foreach (var obj in GameEngine.Current.AllGameObjects)
    {
        if (obj.States.Destroyed)
            continue;
            
        obj.Update();
    }

    // 更新所有游戏对象的协程
    foreach (var obj in GameEngine.Current.AllGameObjects)
    {
        if (obj.States.Destroyed)
            continue;

        obj.UpdateCoroutines();
    }

    // 将需要被销毁的游戏对象存起来
    objectsToDestroy.Clear();
    foreach (var obj in GameEngine.Current.AllGameObjects)
    {
        if (obj.States.Destroyed)
            objectsToDestroy.Add(obj);
    }

    // 从游戏引擎中移出游戏对象
    foreach (var obj in objectsToDestroy)
        GameEngine.Current._allGameObjects.Remove(obj);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行结果:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Logic out
Coroutine inner 0
Coroutine inner 1
Coroutine inner 2
Coroutine inner 3
Coroutine inner 4
Logic out end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;综上所述, 可以了解到, Unity 协程的本质无非就是在合适的实际执行迭代器的 &lt;code&gt;MoveNext&lt;/code&gt; 方法. 对当前正在等待的对象进行条件判断, 如果满足条件, 则 &lt;code&gt;MoveNext&lt;/code&gt;, 否则就不执行.&lt;/p&gt;
</content:encoded></item><item><title>[C#] 基于 yield 语句的迭代器逻辑懒执行</title><link>https://slimenull.com/posts/20231208192209/</link><guid isPermaLink="true">https://slimenull.com/posts/20231208192209/</guid><description>众所周知, C# 可以通过yield语句来快速向或者类型的方法返回值返回一个元素. 但它还有另外一个特性, 就是其内部逻辑的懒执行. 每两个yield语句之间的逻辑都是一个状态, 只有在调用迭代器的MoveNext方法后, 才会执行下一个状态的逻辑.</description><pubDate>Fri, 08 Dec 2023 19:22:09 GMT</pubDate><content:encoded>&lt;p&gt;众所周知, C# 可以通过 &lt;code&gt;yield&lt;/code&gt; 语句来快速向 &lt;code&gt;IEnumerator&lt;/code&gt; 或者 &lt;code&gt;IEnumerable&lt;/code&gt; 类型的方法返回值返回一个元素. 但它还有另外一个特性, 就是其内部逻辑的懒执行. 每两个 &lt;code&gt;yield&lt;/code&gt; 语句之间的逻辑都是一个状态, 只有在调用迭代器的 &lt;code&gt;MoveNext&lt;/code&gt; 方法后, 才会执行下一个状态的逻辑.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在文章中, 编译后的代码已经经过简化和删减, 以便于理解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;迭代器方法的懒执行&lt;/h2&gt;
&lt;p&gt;举一个简单的例子:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IEnumerator SomeLogic()
{
    Console.WriteLine(&quot;hello world&quot;);
    yield return null;
    Console.WriteLine(&quot;fuck you world&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在调用的时候, 逻辑并不会被立即执行, 只有对其返回的迭代器调用 &lt;code&gt;MoveNext&lt;/code&gt; 的时候, 才会继续执行.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var enumerator = SomeLogic();

enumerator.MoveNext();   // 打印 hello world
enumerator.MoveNext();   // 打印 fuck you world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;迭代器方法的编译&lt;/h2&gt;
&lt;p&gt;在 C# 中, 使用了 yield 语句的, 返回 IEnumerator 或 IEnumerable 的方法, 会由编译器生成一个迭代器或可迭代类型, 在类型的内部包含该方法的逻辑.&lt;/p&gt;
&lt;p&gt;例如上面提到的代码, 它会被编译成大概这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IEnumerator SomeLogic()
{
    return new SomeLogicEnumerator();
}


private sealed class SomeLogicEnumerator : IEnumerator, IDisposable
{
    private int state;
    private object current;

    object IEnumerator.Current
    {
        get
        {
            return current;
        }
    }

    public SomeLogicEnumerator(int state)
    {
        this.state = state;
    }

    void IDisposable.Dispose()
    {
    }

    private bool MoveNext()
    {
        int num = state;
        if (num != 0)
        {
            if (num != 1)
            {
                return false;
            }
            state = -1;
            Console.WriteLine(&quot;AWA&quot;);
            return false;
        }
        state = -1;
        Console.WriteLine(&quot;QWQ&quot;);
        current = null;
        state = 1;
        return true;
    }

    bool IEnumerator.MoveNext()
    {
        //ILSpy generated this explicit interface implementation from .override directive in MoveNext
        return this.MoveNext();
    }

    void IEnumerator.Reset()
    {
        throw new NotSupportedException();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到, 在这个迭代器类型中, 有一个 &lt;code&gt;state&lt;/code&gt; 字段存储了当前的状态, 而在 &lt;code&gt;MoveNext&lt;/code&gt; 被调用时, 会切换当前状态, 然后根据当前状态执行对应逻辑.&lt;/p&gt;
&lt;p&gt;当然, 如果你的迭代器逻辑稍微长一些, 它也是可以处理的.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IEnumerator SomeLogic()
{
    Console.WriteLine(&quot;QWQ&quot;);
    yield return 1;
    Console.WriteLine(&quot;AWA&quot;);
    yield return 2;
    Console.WriteLine(&quot;QJFD&quot;);
    yield return 3;
    Console.WriteLine(&quot;JWOEIJFOIWE&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它的 &lt;code&gt;MoveNext&lt;/code&gt; 就变成了这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private bool MoveNext()
{
    switch (state)
    {
        case 0:
            Console.WriteLine(&quot;QWQ&quot;);
            current = 1;
            state = 1;
            return true;
        case 1:
            Console.WriteLine(&quot;AWA&quot;);
            current = 2;
            state = 2;
            return true;
        case 2:
            Console.WriteLine(&quot;QJFD&quot;);
            current = 3;
            state = 3;
            return true;
        case 3:
            state = -1;
            Console.WriteLine(&quot;JWOEIJFOIWE&quot;);
            return false;
        default:
            return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[Rust] 可迭代类型, 迭代器, 如何正确的创建自定义可迭代类型</title><link>https://slimenull.com/posts/20231203202048/</link><guid isPermaLink="true">https://slimenull.com/posts/20231203202048/</guid><description>1. 对于一次性使用的类型, 可以直接对其实现迭代器 trait.2. 对于容器, 不应该对容器本身直接实现迭代器, 而是应该单独创建迭代器类型, 然后对其本身实现 `IntoIterator`</description><pubDate>Sun, 03 Dec 2023 20:20:48 GMT</pubDate><content:encoded>&lt;p&gt;在 Rust 中, for 语句的执行依赖于类型对于 &lt;code&gt;IntoIterator&lt;/code&gt; 的实现, 如果某类型实现了这个 trait, 那么它就可以直接使用 for 进行循环.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;直接实现&lt;/h2&gt;
&lt;p&gt;在 Rust 中, 如果一个类型实现了 &lt;code&gt;Iterator&lt;/code&gt;, 那么它会被同时实现 &lt;code&gt;IntoIterator&lt;/code&gt;, 具体逻辑是返回自身, 因为自身就是迭代器.&lt;/p&gt;
&lt;p&gt;但是如果自身就是迭代器的话, 就意味着自身必须存储迭代状态, 例如当前迭代的位置. 如果是这样的话, 迭代器就只能被使用一次. 况且自身直接被传入 &lt;code&gt;into_iter&lt;/code&gt; 方法后, 所有权被转移, 该对象就无法被再次使用了.&lt;/p&gt;
&lt;p&gt;定义类型本身:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct IntRange {
    current: i32,
    step: i32,
    end: i32
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接为其实现迭代器:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Iterator for IntRange {
    type Item = i32;

    fn next(&amp;amp;mut self) -&amp;gt; Option&amp;lt;Self::Item&amp;gt; {
        if self.current == self.end {
            return None;
        } else {
            let current = self.current;
            self.current += self.step;
            
            return Some(current);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用该类型:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let range = IntRange { current: 0, step: 1, end: 10 };
for value in range {
    println!(&quot;v: {}&quot;, value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以结论是, 如果你的类型是一次性用品, 你可以直接对其实现 &lt;code&gt;Iterator&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;手动实现迭代器&lt;/h2&gt;
&lt;p&gt;如果你向手动实现类似于容器的东西, 那么它当然不是一次性的. 我们应该仿照 Rust 中对切片的迭代器实现.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;同时实现会转移所有权和不会转移所有权的两个迭代器&lt;/li&gt;
&lt;li&gt;对 &lt;code&gt;self&lt;/code&gt; 和 &lt;code&gt;&amp;amp;self&lt;/code&gt; 都实现 &lt;code&gt;IntoIterator&lt;/code&gt;, 这样就可以做不转移所有权的迭代了&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;类型本身:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct IntRange {
    step: i32,
    end: i32
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个迭代器:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct IntRangeIter&amp;lt;&apos;a&amp;gt; {
    range: &amp;amp;&apos;a IntRange,
    current: i32,
}

struct IntRangeIntoIter {
    range: IntRange,
    current: i32,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个迭代器实现:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Iterator for IntRangeIter&amp;lt;&apos;_&amp;gt; {
    type Item = i32;
    
    fn next(&amp;amp;mut self) -&amp;gt; Option&amp;lt;Self::Item&amp;gt; {
        if self.current == self.range.end {
            return None;
        } else {
            let current = self.current;
            self.current += self.range.step;
            return Some(current);
        }
    }
}

impl Iterator for IntRangeIntoIter {
    type Item = i32;
    
    fn next(&amp;amp;mut self) -&amp;gt; Option&amp;lt;Self::Item&amp;gt; {
        if self.current == self.range.end {
            return None;
        } else {
            let current = self.current;
            self.current += self.range.step;
            return Some(current);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现返回两种迭代器的 &lt;code&gt;IntoIterator&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl&amp;lt;&apos;a&amp;gt; IntoIterator for &amp;amp;&apos;a IntRange {
    type Item = i32;
    type IntoIter = IntRangeIter&amp;lt;&apos;a&amp;gt;;

    fn into_iter(self) -&amp;gt; Self::IntoIter {
        IntRangeIter {
            range: self,
            current: 0
        }
    }
}

impl IntoIterator for IntRange {
    type Item = i32;
    type IntoIter = IntRangeIntoIter;
    
    fn into_iter(self) -&amp;gt; Self::IntoIter {
        IntRangeIntoIter {
            range: self,
            current: 0
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用它:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let range = IntRange { step: 1, end: 10 };

// 可以使用引用来进行 for 循环
for value in &amp;amp;range {
    println!(&quot;v: {}&quot;, value);
}

// 也可以直接对其进行 for 循环
for value in range {
    println!(&quot;v: {}&quot;, value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;切片对迭代的实现&lt;/h2&gt;
&lt;p&gt;我们知道, Rust 的切片有一个 &lt;code&gt;iter&lt;/code&gt; 方法, 其实它就相当于对当前切片的引用调用 &lt;code&gt;into_iter&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;其实, 在调用切片引用的 &lt;code&gt;into_iter&lt;/code&gt; 方法时, 本质上就是调用的其 &lt;code&gt;iter&lt;/code&gt; 方法. 方法的实现是在 &lt;code&gt;iter&lt;/code&gt; 内的.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let v = vec![1, 2, 3];

// 下面两个调用是等价的
let iter1 = v.iter();
let iter2 = (&amp;amp;v).into_iter();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你希望实现迭代变量可变的迭代器, 还可以为 &lt;code&gt;&amp;amp;mut T&lt;/code&gt; 实现 &lt;code&gt;into_iter&lt;/code&gt;, 当然, Rust 内部对于切片的实现, 也是这样的:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let mut v = vec![1, 2, 3];

// 下面两个调用是等价的
let mutIter = v.iter_mut();
let mutIter = (&amp;amp;mut v).into_iter();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;两种类型:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;对于一次性使用的类型, 可以直接对其实现迭代器 trait.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于容器, 不应该对容器本身直接实现迭代器, 而是应该单独创建迭代器类型, 然后对其本身实现 &lt;code&gt;IntoIterator&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了方便用户使用, 调用之间的实现应该是这样:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;实现 &lt;code&gt;T&lt;/code&gt; 的 &lt;code&gt;IntoIterator&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;实现 &lt;code&gt;&amp;amp;T&lt;/code&gt; 的 &lt;code&gt;iter&lt;/code&gt; 函数, 返回借用的迭代器.&lt;/li&gt;
&lt;li&gt;实训 &lt;code&gt;&amp;amp;mut T&lt;/code&gt; 的 &lt;code&gt;iter_mut&lt;/code&gt; 函数, 返回可变借用的迭代器.&lt;/li&gt;
&lt;li&gt;对 &lt;code&gt;&amp;amp;T&lt;/code&gt; 和 &lt;code&gt;&amp;amp;mut T&lt;/code&gt; 实现 &lt;code&gt;into_iter&lt;/code&gt; 函数, 并在内部调用刚刚实现的 &lt;code&gt;iter&lt;/code&gt; 和 &lt;code&gt;iter_mut&lt;/code&gt; 函数.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样, 用户就可以直接调用 &lt;code&gt;iter&lt;/code&gt; 方法获得借用的迭代器, 然后使用 &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt; 等方法进行集合的复杂操作了&lt;/p&gt;
</content:encoded></item><item><title>[Rust] 快速基础入门教程</title><link>https://slimenull.com/posts/20231130141912/</link><guid isPermaLink="true">https://slimenull.com/posts/20231130141912/</guid><description>Rust 是一个无运行时的强类型语言, 包含很多高级特性, 例如泛型, lambda 等. 又因为其独有的所有权机制, 所以 Rust 的内存安全要比 C++ 完善许多.</description><pubDate>Thu, 30 Nov 2023 14:19:12 GMT</pubDate><content:encoded>&lt;p&gt;Rust 是一个无运行时的强类型语言, 包含很多高级特性, 例如泛型, lambda 等. 又因为其独有的所有权机制, 所以 Rust 的内存安全要比 C++ 完善许多.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;风格&lt;/h2&gt;
&lt;p&gt;Rust 与 C 族语言不一样, C 族语言在定义方法, 变量时, 都是 &lt;code&gt;类型 关键字&lt;/code&gt; 这样的格式, 也就是类型前置. Rust 采用的是类型后置的风格, 即 &lt;code&gt;关键字: 类型&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;基本结构&lt;/h2&gt;
&lt;p&gt;Rust 的结构与 C++ 是差不多的, 一个文件的顶部写要引入的内容, 下面是结构, 函数, 特征的声明.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::io::stdout;
use std::io::Write;

fn main() {
    let hello_str = &quot;Hello world&quot;;
    let bytes = hello_str.as_bytes();

    stdout().write_all(bytes).unwrap();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面代码中, &lt;code&gt;std::io::stdout&lt;/code&gt; 与 &lt;code&gt;std::io::Write&lt;/code&gt; 都是在 &lt;code&gt;std::io&lt;/code&gt; 下, 它们可以通过花括号合并为以下语句:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::io::{Write, stdout};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;基本类型&lt;/h2&gt;
&lt;p&gt;数字类型&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;长度&lt;/th&gt;
&lt;th&gt;有符号&lt;/th&gt;
&lt;th&gt;无符号&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8 位&lt;/td&gt;
&lt;td&gt;i8&lt;/td&gt;
&lt;td&gt;u8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 位&lt;/td&gt;
&lt;td&gt;i16&lt;/td&gt;
&lt;td&gt;u16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32 位&lt;/td&gt;
&lt;td&gt;i32&lt;/td&gt;
&lt;td&gt;u32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64 位&lt;/td&gt;
&lt;td&gt;i64&lt;/td&gt;
&lt;td&gt;u64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128 位&lt;/td&gt;
&lt;td&gt;i128&lt;/td&gt;
&lt;td&gt;u128&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;平台大小&lt;/td&gt;
&lt;td&gt;isize&lt;/td&gt;
&lt;td&gt;usize&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;以及三十二位浮点数 &lt;code&gt;f32&lt;/code&gt; 与六十四位浮点数 &lt;code&gt;f64&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;布尔(逻辑)值 &lt;code&gt;bool&lt;/code&gt;, 字符(Unicode)值 &lt;code&gt;char&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Rust 中, 一个字符占四个字节, 可以表达任何 Unicode 字符, 包含 Emoji 表情.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;函数声明&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;fn&lt;/code&gt; 关键字来声明一个函数.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn test1() {
    println!(&quot;hello&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要带参数, 直接按照类型后置风格在括号内写明.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn test2(number: i32) {
    println!(&quot;number: {}&quot;, number);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要带返回值, 直接使用箭头 &lt;code&gt;-&amp;gt;&lt;/code&gt; 来指定.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn test3(num1: i32, num2: i32) -&amp;gt; i32 {
    return num1 + num2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当返回值是最后一行的时候, 你可以省略 return 和结尾的分号, 直接将表达式作为返回值返回.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn test3(num1: i32, num2: i32) -&amp;gt; i32 {
    num1 + num2
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果参数或者返回值是函数类型, 使用 &lt;code&gt;fn&lt;/code&gt; 关键字即可, 下面的例子中, resolver 是一个函数, 这个函数有两个 &lt;code&gt;i32&lt;/code&gt; 参数, 返回值是 &lt;code&gt;i32&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn test4(num1: i32, num2: i32, resolver: fn(i32, i32) -&amp;gt; i32) {
    println!(&quot;{}&quot;, resolver(num1, num2));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;变量声明&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;let&lt;/code&gt; 声明一个变量.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let num: i32 = 114514;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大部分情况, 你都可以省略掉类型标记, Rust 会自动推导它的类型.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let num = 114514;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面声明的变量, 是不可变的. 如果你希望声明可变的变量, 需要使用 &lt;code&gt;mut&lt;/code&gt; 关键字.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let mut num = 114514;

// 更改其值
num = 666;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;流程控制&lt;/h2&gt;
&lt;p&gt;Rust 中的 &lt;code&gt;if&lt;/code&gt; 不使用括号, 直接跟表达式以及语句即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let num = 114514;
if num == 114514 {
    println!(&quot;value is 114514&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样, Rust 中也有 &lt;code&gt;else&lt;/code&gt;, &lt;code&gt;else if&lt;/code&gt; 可用, 和 C 族语言类似, 只不过是少了括号.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if num == 114514 {
    println!(&quot;value is 114514&quot;);
} else if num == 1919810 {
    println!(&quot;value is 1919810&quot;);
} else {
    println!(&quot;invalid value&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rust 中的 &lt;code&gt;if&lt;/code&gt; 也可以实现根据条件返回特定值的需求.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let num = 114514;
let tip = if num == 114514 {
    &quot;哼哼哼&quot;
} else {
    &quot;啊啊啊&quot;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的例子中, 对 &lt;code&gt;num&lt;/code&gt; 进行判断, 如果值为 114514, tip 的值会是 &quot;哼哼哼&quot;, 否则为 &quot;啊啊啊&quot;. 需要注意的是, 当你希望 &lt;code&gt;if&lt;/code&gt; 语句将语句作为结果返回时, 不要在语句末尾添加分号.&lt;/p&gt;
&lt;p&gt;Rust 中的  &lt;code&gt;for&lt;/code&gt; 用来对一个实例进行迭代. 使用 &lt;code&gt;起始值..结束值&lt;/code&gt; 这样的语法可以创建简单的数值范围. 搭配 &lt;code&gt;for&lt;/code&gt; 即可实现简单的数值循环.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for i in 0..10 {
    println!(&quot;current value: {}&quot;, i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的, Rust 中, &lt;code&gt;continue&lt;/code&gt; 和 &lt;code&gt;break&lt;/code&gt; 也可用.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for i in 0..10 {
    println!(&quot;current value: {}&quot;, i);

    if i == 3 {
        continue;
    }

    if i == 7 {
        break;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;值得一提的是, Rust 在循环时, 是允许对集合元素进行修改的. 只需要将迭代变量使用 &lt;code&gt;mut&lt;/code&gt; 修饰.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let arr = [1, 2, 3, 4];
for mut ele in arr {
    ele = ele * 2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你需要一个 &apos;死循环&apos;, 可以直接使用 &lt;code&gt;loop&lt;/code&gt; 语句.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let mut i = 0;
loop {
    println!(&quot;current value: {}&quot;, i);

    if i == 3 {
        continue;
    }

    if i == 10 {
        break;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rust 的 &lt;code&gt;loop&lt;/code&gt; 还支持给循环语句加上标签, 然后在内部循环中直接中断指定标签的循环.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&apos;loop_out: loop {
    loop {
        // 在内部循环直接中断最外部循环
        break &apos;loop_out;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rust 的 &lt;code&gt;loop&lt;/code&gt; 还可以作为一个带返回值的表达式使用. 只需要在 break 的时候提供返回值即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let mut value = 0;
let result = loop {
    value += 1;

    if value == 10 {
        break value * 2;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rust 的 &lt;code&gt;match&lt;/code&gt; 语句可以近似理解为 &lt;code&gt;if&lt;/code&gt; 的高级语法. 传入一个值, 以及匹配条件和语句, 可以执行对应语句.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let num = 114514;
match num {
    114514 =&amp;gt; println!(&quot;hello&quot;),
    1919810 =&amp;gt; println!(&quot;world&quot;),
    _ =&amp;gt; { }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的例子中, 会对 num 进行匹配, 并且在值为 114514 和 1919810 时执行不同的语句, 如果所有条件都没有匹配到, 则会使用 &lt;code&gt;_ =&amp;gt; { }&lt;/code&gt; 表示的默认情况, 在这里是空语句, 也就是什么也不执行.&lt;/p&gt;
&lt;p&gt;同时, Rust 的 &lt;code&gt;match&lt;/code&gt; 语句也可以作为表达式返回一个值, 只需要 &lt;code&gt;match&lt;/code&gt; 内的语句是有返回值的表达式即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let result = match num {
    114514 =&amp;gt; &quot;hello&quot;,
    1919810 =&amp;gt; &quot;world&quot;,
    _ =&amp;gt; &quot;&quot;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的示例中, &lt;code&gt;match&lt;/code&gt; 对 num 进行匹配, 并且在值为 114514 和 1919810 的时候返回不同的字符串, 最终赋值给 result. 如果没有匹配到指定条件, 则是使用默认语句 &lt;code&gt;_ =&amp;gt; &quot;&quot;&lt;/code&gt; 返回一个空的字符串.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是, 在 &lt;code&gt;match&lt;/code&gt; 语句中, 使用的是 &lt;code&gt;=&amp;gt;&lt;/code&gt; 而不是 &lt;code&gt;-&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;字符串 / Strings&lt;/h2&gt;
&lt;p&gt;在 Rust 中, 字符串分两种, 一种是 &lt;code&gt;str&lt;/code&gt;, 它表示字符串本身, 不可变.
由于 &lt;code&gt;str&lt;/code&gt; 作为字符串本身, 其大小是不确定的, 所以它无法作为本地变量存储. 我们在使用时, 使用的都是 &lt;code&gt;&amp;amp;str&lt;/code&gt;, 也就是 &lt;code&gt;str&lt;/code&gt; 的引用.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let hello1 : &amp;amp;str = &quot;你好世界&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另一种是 &lt;code&gt;String&lt;/code&gt;, 本质是数组的包装, 它是可变的. 你可以对其进行更改. 你可以将它理解为其他语言中常见的 &lt;code&gt;StringBuilder&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let hello2 : String = String::new();

hello2.add(&quot;向字符串中添加一些内容&quot;);
hello2.add(&quot;, 你好吗?&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你需要将字符串编码为字节数组, 可以直接使用 &lt;code&gt;as_bytes&lt;/code&gt; 函数&lt;/p&gt;
&lt;p&gt;由于 Rust 中字符串使用 UTF-8 存储, 所以该函数的结果即为字符串使用 UTF-8 编码后的结果.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let tip = &quot;hello world&quot;;
let bytes = tip.as_bytes();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望从 UTF-8 转为 Rust 字符串, 可以使用 &lt;code&gt;std::str::from_utf8&lt;/code&gt; 函数进行转换.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// bytes 为需要解码的数据
let bytes : &amp;amp;[u8];
let some_str = std::str::from_utf8(bytes);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;数组 / Arrays&lt;/h2&gt;
&lt;p&gt;在 Rust 中, 数组是长度不可变的容器, 并且其大小必须在编译时确定.  其类型表达为: &lt;code&gt;[类型; 长度]&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let arr : [i32; 4];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在使用这个数组之前, 我们还需要对其进行初始化, 可以使用中括号指定其每一个元素的值.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let arr : [i32; 4] = [1, 2, 3, 4];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然, 这里的数组类型也可以被省略掉.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let arr = [1, 2, 3, 4];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你希望直接初始化一个指定长度的数组, 可以使用中括号以及分号. 就像数组的类型表示.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let arr : [i32; 4] = [0; 4];
let arr = [0; 4];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;容器 / Collections&lt;/h2&gt;
&lt;p&gt;如果你需要可变的容器, 可以使用 &lt;code&gt;Vec&amp;lt;T&amp;gt;&lt;/code&gt;, 当然, 只有在声明时使用 &lt;code&gt;mut&lt;/code&gt; 关键字, 它才可变.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let mut v : Vec&amp;lt;i32&amp;gt; = Vec::&amp;lt;i32&amp;gt;::new();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可以将它简写为这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 指定变量类型, Vec 的泛型参数会自动推导
let mut v : Vec&amp;lt;i32&amp;gt; = Vec::new();

// 指定泛型参数, 变量的类型会自动推导.
let mut v = Vec::&amp;lt;i32&amp;gt;::new();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;len&lt;/code&gt; 函数获取其长度:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let len = v.len();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 &lt;code&gt;push&lt;/code&gt; 和 &lt;code&gt;pop&lt;/code&gt; 方法可以在 &lt;code&gt;Vec&amp;lt;T&amp;gt;&lt;/code&gt; 的结尾增删元素.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;v.push(114514);
v.pop();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;insert&lt;/code&gt; 和 &lt;code&gt;remove&lt;/code&gt; 可以在指定位置增删元素.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;v.insert(0, 114514);
v.remove(0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哈希映射(HashMap)用于存储基于哈希值的键值映射, 像是其他语言中的 &quot;Dictionary&quot; 或者 &quot;Hashtable&quot;,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let mut hm : HashMap&amp;lt;&amp;amp;str, &amp;amp;str&amp;gt; = HashMap::&amp;lt;&amp;amp;str, &amp;amp;str&amp;gt;::new();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简写:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let mut hm : HashMap&amp;lt;&amp;amp;str, &amp;amp;str&amp;gt; = HashMap::new();
let mut hm = HashMap::&amp;lt;&amp;amp;str, &amp;amp;str&amp;gt;::new();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;len&lt;/code&gt; 函数获取其长度:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let len = hm.len();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;insert() 方法用于插入或更新一个键值对到哈希映射中, 如果键已经存在, 则更新为新的键值对, 并则返回旧的值. 如果键不存在则执行插入操作并返回 None.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hm.insert(&quot;qwq&quot;, &quot;awa&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从哈希映射中获取和删除值.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let valueOption = hm.get(&amp;amp;&quot;qwq&quot;);
let valueOption = hm.remove(&amp;amp;&quot;qwq&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以使用 &lt;code&gt;for&lt;/code&gt; 对哈希映射进行循环:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (k, v) in hm { 
    println!(&quot;Key: {}, Value: {}&quot;, k, v);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哈希集合是基于哈希值的元素不重复容器, 常用于去重或快速查找元素是否存在.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let mut hs: HashSet&amp;lt;i32&amp;gt; = HashSet::new();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取长度, 插入数据, 删除数据, 判断数据是否已经存在:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let len = hs.len();
let result = hs.insert(123);
let result = hs.remove(&amp;amp;123);
let result = hs.contains(&amp;amp;123);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;结构 / Structures&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;struct&lt;/code&gt; 关键字可以创建一个结构体.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Point {
    x: i32,
    y: i32,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在使用结构体类型的变量时, 该变量必须被初始化.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let p : Point = Point { x: 1, y: 3 };
let p = Point { x: 1, y: 3 };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可以对它的成员进行赋值, 取值.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p.x = 114514;
let x = p.x;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要为该类型添加一些方法, 使用 &lt;code&gt;impl&lt;/code&gt; 关键字.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Point {
    fn new(x: i32, y: i32) -&amp;gt; Point {
        Point { x: x, y: y }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在, 你可以使用 &lt;code&gt;Point::new&lt;/code&gt; 来创建一个 &lt;code&gt;Point&lt;/code&gt; 了.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let p = Point::new(123, 456);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你要为该类型的实例创建一些函数, 只需要在编写函数时, 将第一个参数声明为 &lt;code&gt;self&lt;/code&gt; 即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Point {
    fn output(self: &amp;amp;Self) {
        println!(&quot;Point, x: {}, y: {}&quot;, self.x, self.y);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在你可以通过一个 &lt;code&gt;Point&lt;/code&gt; 实例来调用 &lt;code&gt;output&lt;/code&gt; 函数进行输出了.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p.output();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上述代码中, &lt;code&gt;self&lt;/code&gt; 关键字表示当前实例, &lt;code&gt;Self&lt;/code&gt; 关键字表示当前类型, 当然, 你也可以将它写成具体的类型. 下面的代码都是有效的实例函数定义:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Point {
    // 不使用 Self 关键字, 而是使用具体的 Point 类型
    fn output1(self: &amp;amp;Point) { }

    // 不使用 Self 关键字, 而是让其自动推导类型
    fn output2(&amp;amp;self) { }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;特征 / Traits&lt;/h2&gt;
&lt;p&gt;在 Rust 中, &lt;code&gt;trait&lt;/code&gt; 表示某种特征. 例如 &quot;可迭代&quot;, &quot;可显示&quot;, &quot;可调试&quot;. 它类似于其他编程语言的接口. 使用 &lt;code&gt;trait&lt;/code&gt; 关键字创建一个特征.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;trait TestTrait {
    fn some_func();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的例子中, 我们创建了一个名为 &lt;code&gt;TestTrait&lt;/code&gt; 的 &lt;code&gt;trait&lt;/code&gt;, 它规定, 需要有一个名为 &lt;code&gt;some_func&lt;/code&gt; 的无参无返回值函数.&lt;/p&gt;
&lt;p&gt;要使某个结构实现一个 &lt;code&gt;trait&lt;/code&gt;, 使用 &lt;code&gt;impl ... for ...&lt;/code&gt; 语法.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl TestTrait for Point {
    fn some_func() {
        println!(&quot;hello world from struct Point&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;trait&lt;/code&gt; 主要是与泛型搭配使用. 同时和其他编程语言不一样的是, &lt;code&gt;trait&lt;/code&gt; 无法直接作为一个函数的参数类型. 你需要使用泛型, 然后指定泛型需要实现某 &lt;code&gt;trait&lt;/code&gt;, 然后将该泛型作为函数的参数类型使用.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;trait&lt;/code&gt; 可以用内置的实现, 在这方面, 它又像其他语言的抽象类. 例如, 某个 &lt;code&gt;trait&lt;/code&gt; 需要类型实现函数 A, 而函数 B 由该 &lt;code&gt;trait&lt;/code&gt; 自己实现, 内部逻辑依赖于函数 A. 这时, 只要某个类型实现了这个 trait 并编写函数 A 的实现, 他就可以直接使用 &lt;code&gt;trait&lt;/code&gt; 内的函数 B.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;trait TestTrait {
    fn get_string(&amp;amp;self) -&amp;gt; String;
    fn print_string(&amp;amp;self) {
        println!(&quot;{}&quot;, self.get_string());
    }
}

impl TestTrait for Point {
    fn get_string(&amp;amp;self) -&amp;gt; String {
        return format!(&quot;Point, x: {}, y: {}&quot;, self.x, self.y);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;let p = Point::new(1, 2);
p.print_string();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;特性 / Attributes&lt;/h2&gt;
&lt;p&gt;Rust 中的特性(Attribute)是一种标记. 类似于 C# 的 &lt;code&gt;Attribute&lt;/code&gt; 或者 Python 中的 &lt;code&gt;Decorators&lt;/code&gt;, 在做上标记后, 即可拥有某种行为.&lt;/p&gt;
&lt;p&gt;下面使用 &lt;code&gt;derive&lt;/code&gt; 特性演示特性的使用.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[derive(PartialEq, Eq)]
struct Point {
    x: i32,
    y: i32
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在以上代码中, 我们为 Point 结构添加了 derive 特性, 这个特性用于自动实现指定的 &lt;code&gt;trait&lt;/code&gt;, 在这里, 我们指定了 &lt;code&gt;PartialEq&lt;/code&gt; 和 &lt;code&gt;Eq&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;在实现了 &lt;code&gt;PartialEq&lt;/code&gt; 和 &lt;code&gt;Eq&lt;/code&gt; 后, 我们的 Point 结构现在可以使用 &lt;code&gt;==&lt;/code&gt; 和 &lt;code&gt;!=&lt;/code&gt; 运算符了.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let p1 = Point { x: 123, y: 456 };
let p2 = Point { x: 345, y: 829 };

if p1 == p2 {
    println!(&quot;两点相等&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;枚举 / Enumerations&lt;/h2&gt;
&lt;p&gt;Rust 中的枚举和其他语言中的枚举有很大不同. 它枚举可理解为 &quot;情况&quot;, 既可以作为类似于 C# 中的纯值类型使用, 也可以像 Java 的枚举一样在枚举中存储数据.&lt;/p&gt;
&lt;p&gt;Rust 对于枚举的优化是很好的, 不像 Java 一般是基于堆中存储的.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum ColorChannel {
    Red, Green, Blue
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在以上的例子中, 我们声明了一个最简单的枚举, 这个枚举仅包含三种情况, 即 &apos;红&apos;, &apos;绿&apos;, &apos;蓝&apos;. 在这种情况下, 你可以理解为我们定义了三个数字值常量, 通过 &lt;code&gt;ColorChannel&lt;/code&gt; 可以访问它们.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let color_channel1 = ColorChannel::Red;
let color_channel2 = ColorChannel::Green;
let color_channel3 = ColorChannel::Blue;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用模式匹配进行判断.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;match color_channel1 {
    ColorChannel::Red =&amp;gt; println!(&quot;is red&quot;),
    _ =&amp;gt; {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里我们不能使用 &lt;code&gt;if&lt;/code&gt; 语句进行判断, 因为我们定义的枚举没有实现名为 &lt;code&gt;PartialEq&lt;/code&gt; 的 &lt;code&gt;trait&lt;/code&gt;, 这是内置于 &lt;code&gt;rust&lt;/code&gt; 的 &lt;code&gt;trait&lt;/code&gt;, 用于重载 &lt;code&gt;==&lt;/code&gt; 和 &lt;code&gt;!=&lt;/code&gt; 运算符.&lt;/p&gt;
&lt;p&gt;下面我们将以一个不同情况的颜色讲述 Rust 中枚举存储值的用法.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Color {
    Rgb(u8, u8, u8),
    Channel(ColorChannel)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的示例中, &lt;code&gt;Color&lt;/code&gt; 分成了两种情况, 一种是 &lt;code&gt;Rgb&lt;/code&gt;, 一种是 &lt;code&gt;Channel&lt;/code&gt;. 当是 &lt;code&gt;Rgb&lt;/code&gt; 的时候, 它存储三个无符号八位整数, 当是 &lt;code&gt;Channel&lt;/code&gt; 的时候, 它存储一个 &lt;code&gt;ColorChannel&lt;/code&gt; 枚举.&lt;/p&gt;
&lt;p&gt;我们可以这样使用它:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let color = Color::Rgb(89, 43, 233);

match color {
    Color::Rgb(r, g, b) =&amp;gt; {
        println!(&quot;颜色是 RGB 值. R: {}, G: {}, B: {}&quot;, r, g, b);
    },

    Color::Channel(channel) =&amp;gt; {
        println!(&quot;颜色是通道, {}&quot;, channel);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意, 因为这里需要将 &lt;code&gt;ColorChannel&lt;/code&gt; 打印输出, 所以 &lt;code&gt;ColorChannel&lt;/code&gt; 需要实现名为 &lt;code&gt;Display&lt;/code&gt; 的 &lt;code&gt;trait&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在这种有存储值的情况下, 我们也可以使用 &lt;code&gt;if let&lt;/code&gt; 的语句对其进行判断:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if let Color::Rgb(r, g, b) = color {
    println!(&quot;颜色是 RGB 值. R: {}, G: {}, B: {}&quot;, r, g, b);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们还可以为枚举中的值命名, 这样就可以:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Color {
    Rgb { r: u8, g: u8, b:u8 },
    Channel { channel: ColorChannel }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过这样的话, 使用方式也需要做些改动:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let color = Color::Rgb { r: 23, g: 12, b: 129 };

match color {
    Color::Rgb { r, g, b } =&amp;gt; {
        println!(&quot;颜色是 RGB 值. R: {}, G: {}, B: {}&quot;, r, g, b);
    },

    Color::Channel { channel }=&amp;gt; {
        println!(&quot;颜色是通道, {}&quot;, channel);
    }
}

if let Color::Rgb { r, g, b } = color {
    println!(&quot;颜色是 RGB 值. R: {}, G: {}, B: {}&quot;, r, g, b);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;泛型 / Generic&lt;/h2&gt;
&lt;p&gt;泛型是编程语言中极重要的一个概念. 通过使用泛型, 可以实现一些逻辑的复用. 例如, 当我们自定义的结构实现 Rust 内置的某些 &lt;code&gt;trait&lt;/code&gt; 时, 我们也可以使用 Rust 内的某些函数.&lt;/p&gt;
&lt;p&gt;下面我们将自己定义一个简单的 &lt;code&gt;trait&lt;/code&gt; 和一个简单的泛型函数.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;trait I32Printer {
    fn print(&amp;amp;self, value: i32);
}

fn print_i32&amp;lt;Printer: I32Printer&amp;gt;(value: i32, printer: Printer) {
    printer.print(value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的逻辑中, 我们在 &lt;code&gt;print_i32&lt;/code&gt; 后添加了尖括号, 尖括号中, 冒号的前半部分表示需要使用的泛型类型, 冒号后面是对泛型类型的约束, 表示该泛型类型必须实现 &lt;code&gt;I32Printer&lt;/code&gt; 这个 &lt;code&gt;trait&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;在参数列表中, 我们定义了 &lt;code&gt;printer&lt;/code&gt; 参数, 指定其类型为我们定义的泛型类型. 这样, 它就可以接受任何实现了 &lt;code&gt;I32Printer&lt;/code&gt; 的类型.&lt;/p&gt;
&lt;p&gt;接下来我们定义两个结构, 实现 &lt;code&gt;I32Printer&lt;/code&gt;, 编写不同的打印逻辑.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct SimpleI32Printer;
struct AnotherI32Printer&amp;lt;&apos;a&amp;gt; {
    prompt: &amp;amp;&apos;a str,
}

impl I32Printer for SimpleI32Printer {
    fn print(&amp;amp;self, value: i32) {
        println!(&quot;{}&quot;, value);
    }
}

impl I32Printer for AnotherI32Printer&amp;lt;&apos;_&amp;gt; {
    fn print(&amp;amp;self, value: i32) {
        println!(&quot;{}: {}&quot;, self.prompt, value);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的例子中, 我们定义了 &lt;code&gt;SimpleI32Printer&lt;/code&gt; 和 &lt;code&gt;AnotherI32Printer&lt;/code&gt; 两个结构, 并且都为它们实现了 &lt;code&gt;I32Printer&lt;/code&gt; 的 &lt;code&gt;trait&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;现在, 可以调用方法, 传入不同的 printer, 然后查看运行结果了.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let printer1 = SimpleI32Printer;
let printer2 = AnotherI32Printer { prompt: &quot;Number:&quot; };

print_i32::&amp;lt;SimpleI32Printer&amp;gt;(114514, printer1);
print_i32::&amp;lt;AnotherI32Printer&amp;gt;(1919810, printer2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到, 它根据我们传入的不同类型实例, 有了不同的行为.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在其他语言, 例如 C# 和 Java 中, 你可以直接将接口作为参数类型指定. 但是在 Rust 中, 你必须创建一个泛型参数来做这样的逻辑.
Rust 中, 一切参数, 变量的大小都应该是固定的. 倘若我们允许 &lt;code&gt;trait&lt;/code&gt; 作为参数类型, 那么类型的大小将不再确定. 而泛型则类似于 C++ 的模板, 在编译时, Rust 编译器会对其做处理, 生成能使用多个类型进行调用的函数.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接下来就是泛型类型了, 在定义类型的时候, 我们也可以使用泛型.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct TwoValues&amp;lt;T1, T2&amp;gt; {
    value1: T1,
    value2: T2
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用起来也很简单:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let two_values = TwoValues::&amp;lt;i32, u8&amp;gt; {
    value1: 123,
    value2: 123
};

// 或者
let two_values : TwoValues&amp;lt;i32, u8&amp;gt; = TwoValues {
    value1: 123,
    value2: 123
};

// 也可以自动推导类型, 这里将会被推导为 TwoValues&amp;lt;i32, i32&amp;gt;
let two_values = TwoValues {
    value1: 123,
    value2: 123
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为泛型类型实现方法, 需要这样写:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl&amp;lt;T1, T2&amp;gt; TwoValues&amp;lt;T1, T2&amp;gt; {
    fn common_fn(&amp;amp;self) {
        println!(&quot;common func&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的例子中, 由于我们不知道泛型类型具体类型, 所以在 &lt;code&gt;impl&lt;/code&gt; 语句后还是需要声明两个泛型类型, 然后传入到类型.&lt;/p&gt;
&lt;p&gt;但如果你希望为带有指定泛型参数的泛型类型定义一些函数, 可以这样写:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl TwoValues&amp;lt;&amp;amp;str, i32&amp;gt; {
    fn test_output(&amp;amp;self) {
        println!(&quot;{}: {}&quot;, self.value1, self.value2);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的例子中, 因为我们只想为泛型类型参数为 &lt;code&gt;&amp;amp;str&lt;/code&gt; 和 &lt;code&gt;i32&lt;/code&gt; 的 &lt;code&gt;TwoValues&lt;/code&gt; 定义函数, 泛型类型已知, 所以不必再定义泛型类型.&lt;/p&gt;
&lt;p&gt;下面还有个例子可供参考, 第一个泛型类型参数我们指定为 &lt;code&gt;&amp;amp;str&lt;/code&gt;, 第二个指定为实现了 &lt;code&gt;Display&lt;/code&gt; 的泛型类型.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl&amp;lt;T: Display&amp;gt; TwoValues&amp;lt;&amp;amp;str, T&amp;gt; {
    fn test_output2(&amp;amp;self) {
        println!(&quot;{}: {}&quot;, self.value1, self.value2)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是, 与其他语言不一样, Rust 在构造类型实例或者调用泛型函数的时候, 需要使用两个冒号以及尖括号来指定泛型类型参数.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 正确使用
let two_values = TwoValues::&amp;lt;&amp;amp;str, i32&amp;gt; {
    value1: &quot;Tip&quot;,
    value2: 10
}

print_i32::&amp;lt;SimpleI32Printer&amp;gt;(114514, printer1);

// 错误使用
let two_values = TwoValues&amp;lt;&amp;amp;str, i32&amp;gt; {
    value1: &quot;Tip&quot;,
    value2: 10
}

print_i32&amp;lt;SimpleI32Printer&amp;gt;(114514, printer1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之所以强调这点, 是因为其他语言, 诸如 C#, Java, Kotlin, 它们在构造类型实例和调用泛型方法的时候, 都是直接使用尖括号来指定泛型类型参数的. Rust 需要多加两个冒号, 初学者可能会忘记这点.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;所有权 / Ownership&lt;/h2&gt;
&lt;p&gt;为了保证内存安全, Rust 引入了 &apos;所有权&apos; 的概念. 其大概思想为:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一个类型实例有唯一的作用域, 当离开其作用域时, 该实例会被销毁
这个作用域称为它的 &apos;所有者&apos;, 该作用域持有该实例的 &apos;所有权&apos;&lt;/li&gt;
&lt;li&gt;所有权可以转交给另一个作用域, 转交后, 当前作用域将无法继续使用该实例&lt;/li&gt;
&lt;li&gt;所有权可以借用, 并且指定一定的访问权限, 当前作用域仍持有该实例的 &apos;所有权&apos;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;大多数编程语言都有作用域的概念, 离开作用域后, 值将作废:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if true {
    let some_integer = 114514;
}

// 这里将报错, 因为已经脱离了 some_integer 的作用域
println!(&quot;value: {}&quot;, some_integer);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当一个值直接传入到另外一个函数中, 那么这个值的所有权也将转交到另外一个函数中:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct MyValue {
    value: i32
}

fn print_value(value: MyValue) {
    println!(&quot;value: {}&quot;, value.value);
}

fn main() {
    let my_value = MyValue { value: 114514 };
    print_value(my_value);

    // 这里将报错, 因为在执行 print_value 的时候, 所有权已经被转让
    // 当前作用域不再持有 my_value, 也就无法再使用它
    println!(&quot;value: {}&quot;, my_value.value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望函数不转让传入参数的所有权, 可以将参数类型定义为 &apos;引用&apos;. 你可以将其理解为其他语言中的 &apos;指针&apos;. 只需要在类型前加 &lt;code&gt;&amp;amp;&lt;/code&gt; 符号即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct MyValue {
    value: i32
}

fn print_value(value: &amp;amp;MyValue) {
    println!(&quot;value: {}&quot;, value.value);
}

fn main() {
    let my_value = MyValue { value: 114514 };
    print_value(&amp;amp;my_value);

    // 这时, 你仍然可以使用 my_value
    // 因为当前作用域持有 my_value 的所有权
    println!(&quot;value: {}&quot;, my_value.value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然我们将值借给了 &lt;code&gt;print_value&lt;/code&gt;, 但在 &lt;code&gt;print_value&lt;/code&gt; 内部, 它只能读取参数的值, 而不能对参数进行修改. 如果你希望它能够修改该实例的值, 需要在类型前添加 &lt;code&gt;mut&lt;/code&gt; 关键字.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn change_value(value: &amp;amp;mut MyValue) {
  value.value = 123123;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在调用时也应该使用 &lt;code&gt;&amp;amp;mut xxx&lt;/code&gt; 来获取该实例的可修改引用.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn main() {
    let my_value = MyValue { value: 114514 };

    change_value(&amp;amp;mut my_value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你需要一个能够对值本身进行更改, 那么在赋值时, 需要在变量名前添加 &lt;code&gt;*&lt;/code&gt; 符号.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn change_int_value(value: &amp;amp;mut i32) {
    *value = 114514;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;错误处理&lt;/h2&gt;
&lt;p&gt;在 Rust 中, 错误分为两种: 可恢复的错误以及不可恢复的错误. 例如, 在将字符串解析为数字时, 如果数字格式不正确, 所引发的错误是程序逻辑上可以处理的. 而类似于内存访问冲突, 栈溢出这种, 就是无法恢复的错误.&lt;/p&gt;
&lt;p&gt;不可恢复的错误会直接导致程序崩溃. 你可以使用 &lt;code&gt;panic&lt;/code&gt; 宏手动引发错误.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;panic!(&quot;oops&quot;);
println!(&quot;test&quot;);   // 这里代码不会被执行, 因为程序已经崩了
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而对于可恢复的错误, Rust 中的函数都会返回一个 &lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt; 来表示可能包含错误值的返回值. 它是一个枚举, 包含两种取值: &lt;code&gt;Ok(T)&lt;/code&gt; 与 &lt;code&gt;Err(E)&lt;/code&gt;, 我们可以通过 &lt;code&gt;match&lt;/code&gt; 语句对其两种情况分别进行处理.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let origin_str = &quot;123&quot;;
let parse_result = origin_str.parse::&amp;lt;i32&amp;gt;();

match parse_result {
    Ok(value) =&amp;gt; println!(&quot;Value is: {}&quot;, value),
    Err(err) =&amp;gt; println!(&quot;Error: {}&quot;, err)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你确定该方法的执行不会出现错误, 也可以使用 &lt;code&gt;unwrap&lt;/code&gt; 函数直接取得正确的值.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let origin_str = &quot;123&quot;;
let parsed_value : i32 = origin_str.parse().unwrap();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是如果尝试对一个错误值使用 &lt;code&gt;unwrap&lt;/code&gt;, 就会引发 &lt;code&gt;panic&lt;/code&gt; 了.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let origin_str = &quot;不是数字&quot;;
let parsed_value: i32 = origin_str.parse().unwrap();   // 这里会直接崩溃, 因为解析是失败的, 无法取得结果值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rust 还提供了一个 &lt;code&gt;?&lt;/code&gt; 操作符用于简化异常处理. 下面的代码是不使用 &lt;code&gt;?&lt;/code&gt; 的.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn mul_input_with_10() -&amp;gt; Result&amp;lt;i32, ParseIntError&amp;gt; {
    let mut input = String::new();
    std::io::stdin().read_line(&amp;amp;mut input).unwrap();

    let valueResult = input.parse::&amp;lt;i32&amp;gt;();

    match valueResult {
        Ok(value) =&amp;gt; Ok(value * 10),
        Err(err) =&amp;gt; Err(err),
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用 &lt;code&gt;?&lt;/code&gt; 的话, 则是这样. 当结果为 &lt;code&gt;Err(E)&lt;/code&gt; 的时候, 会直接将结果作为当前函数的返回值返回, 表达式的结果则是正确的值.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn mul_input_with_10() -&amp;gt; Result&amp;lt;i32, ParseIntError&amp;gt; {
    let mut input = String::new();
    std::io::stdin().read_line(&amp;amp;mut input).unwrap();

    Ok(input.parse::&amp;lt;i32&amp;gt;()? * 10)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;模块&lt;/h2&gt;
&lt;p&gt;模块是 Rust 中组织源代码的方式. 在 Rust 中, 一个文件或者文件夹都可以叫做一个 &quot;模块&quot;.&lt;/p&gt;
&lt;p&gt;例如, 当我有一个 &lt;code&gt;main.rs&lt;/code&gt;, 我希望在里面使用 &lt;code&gt;test.rs&lt;/code&gt; 的成员时:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 这里是 test.rs 的内容

// 公开一个函数
pub fn test_fn() {
    println!(&quot;test fn&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是 &lt;code&gt;main.rs&lt;/code&gt;, 使用 &lt;code&gt;mod&lt;/code&gt; 语句引入模块, 然后使用 &lt;code&gt;use&lt;/code&gt; 语句使用模块中的成员:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 引入 test 模块
mod test;

// 使用模块中的成员
use test::test_fn;

fn main() {
    test_fn();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望将一个文件夹暴露为一个模块的话, 你需要先创建一个文件夹, 然后在文件夹下创建 &lt;code&gt;mod.rs&lt;/code&gt;, 然后编写内容. 在该文件下向外暴露的成员, 即为该模块的成员.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 这里是 test2/mod.rs 的内容

// 公开一个函数
pub fn test_fn() {
    println!(&quot;test fn&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;引用的时候和之前的代码一样, 只需要使用 &lt;code&gt;mod test2&lt;/code&gt; 即可引入 &lt;code&gt;test2&lt;/code&gt; 模块.&lt;/p&gt;
&lt;p&gt;如果你希望在 &lt;code&gt;test2&lt;/code&gt;  文件夹下编写更多的文件, 并向外暴露:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;|- test2
|  |- another.rs
|  -- mod.rs
|
-- main.rs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么任何你想要向外暴露的内容, 都应该在 &lt;code&gt;test2/mod.rs&lt;/code&gt; 下声明好.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 这里是 test2/another.rs 的内容

// 公开一个结构
pub struct AnotherStruct {
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;test2/mod.rs&lt;/code&gt; 中, 你需要导入并公开 &lt;code&gt;another&lt;/code&gt; 这个模块.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 这里是 test2/mod.rs 的内容

// 导入并公开 another 模块
pub mod another;

// 公开属于 test2 的成员
pub fn test_fn() {
    println!(&quot;test fn&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是, 你就可以在 &lt;code&gt;main.rs&lt;/code&gt; 中, 使用 &lt;code&gt;AnotherStruct&lt;/code&gt; 这个类型了.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 导入 test2 模块
mod test2;

// 使用 test2/another 中的 AnotherStruct
use test2::another::AnotherStruct;

fn main() {
    let value = AnotherStruct {};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是, 如果你希望在使用 &lt;code&gt;AnotherStruct&lt;/code&gt; 时, 直接通过 &lt;code&gt;test2::AnotherStruct&lt;/code&gt; 导入, 也可以在 &lt;code&gt;mod.rs&lt;/code&gt; 这样向外公开:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 这里是 test/mod.rs 的内容

// 导入 another 模块, 但是不公开
mod another;

// 使用并公开 another 下的结构
pub use another::AnotherStruct;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样 &lt;code&gt;AnotherStruct&lt;/code&gt; 可以通过 &lt;code&gt;use&lt;/code&gt; 语句直接向外暴露, 使用时就可以直接 &lt;code&gt;use test2::AnotherStruct&lt;/code&gt; 了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 导入 test2 模块
mod test2;

// 使用 test2 直接暴露的 AnotherStruct
use test2::AnotherStruct;

fn main() {
    let value = AnotherStruct {};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方便起见, 你也可以直接用 &lt;code&gt;*&lt;/code&gt; 在 &lt;code&gt;mod.rs&lt;/code&gt; 直接向外暴露某个模块的所以成员:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mod another;

// 向外暴露 another 中的所有成员
pub use another::*;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你在使用多个模块时, 它们的类型名称相同, 你可以在 &lt;code&gt;use&lt;/code&gt; 的使用, 使用 &lt;code&gt;as&lt;/code&gt; 为其取别名:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mod test2;

use test2::AnotherStruct as qwq;

fn main() {
    let value = qwq {};
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[.NET] Speex 语音编解码介绍, 使用, 代码示例</title><link>https://slimenull.com/posts/20231113094519/</link><guid isPermaLink="true">https://slimenull.com/posts/20231113094519/</guid><description>Speex 是一个开源的, 适合语音编解码的算法, 常应用于网络电话中.在下面的的介绍中, 我们将使用对 Speex 编码在 .NET 中的使用做介绍SpeexSharp 可以在 nuget 中直接安装, 并且已经封装了编解码器的类供使用. 如果你不希望了解 Speex 的具体编解码过程, 可以忽略下面的 ‘编码’ 和 ‘解码’ 部分, 只看 Speex 的介绍, 然后直接使用这些类进行编解码.</description><pubDate>Mon, 13 Nov 2023 09:45:19 GMT</pubDate><content:encoded>&lt;p&gt;Speex 是一个开源的, 适合语音编解码的算法, 常应用于网络电话中.&lt;/p&gt;
&lt;p&gt;在下面的的介绍中, 我们将使用 &lt;a href=&quot;https://github.com/SlimeNull/SpeexSharp&quot;&gt;SpeexSharp&lt;/a&gt; 对 Speex 编码在 .NET 中的使用做介绍&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SpeexSharp 可以在 nuget 中直接安装, 并且已经封装了编解码器的类供使用. 如果你不希望了解 Speex 的具体编解码过程, 可以忽略下面的 &apos;编码&apos; 和 &apos;解码&apos; 部分, 只看 Speex 的介绍, 然后直接使用这些类进行编解码.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;采样&lt;/h2&gt;
&lt;p&gt;Speex 的编解码是基于采样的, 传入数据的时候, 我们需要给定采样, 传出的时候, Speex 也是解码为采样.&lt;/p&gt;
&lt;p&gt;Speex 支持的采样格式有两种, 浮点数和有符号 16 位整数.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;模式和质量&lt;/h2&gt;
&lt;p&gt;Speex 目前有三种模式, 窄带, 宽带, 超宽带. 这三种模式中, 对音频数据编码后的数据大小是不一样的. 如其名, 在窄带模式下, 音频编码后最小, 质量也最低, 反之, 超宽带是编码后最大, 质量最好的模式.&lt;/p&gt;
&lt;p&gt;选择好模式之后, 你还可以对编码质量进行微调. 质量的等级是一个从 0 到 10 的值(包含 0 和 10), 设置编码器的质量之后, 编码的结果大小和质量也会变更.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;存储流&lt;/h2&gt;
&lt;p&gt;要进行 Speex 编码, 我们需要一个存储需要编码的数据的缓冲区, Speex 已经为我们准备好了这个类型, 并且 Speex 会自动管理这个缓冲区. 它叫做 &lt;code&gt;SpeexBits&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;初始化一个 &lt;code&gt;SpeexBits&lt;/code&gt;, 我们需要声明一个 &lt;code&gt;SpeexBits&lt;/code&gt; 类型的变量, 然后调用 Speex 的初始化函数.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SpeexBits bits;
Speex.BitsInit(&amp;amp;bits);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;无论是编码还是解码, 都是需要 &lt;code&gt;SpeexBits&lt;/code&gt; 作为存储的.&lt;/p&gt;
&lt;p&gt;编码时, 用户将采样数据的指针传给编码函数, 函数内部对帧进行编码, 最后将编码后的结果放入 &lt;code&gt;SpeexBits&lt;/code&gt; 中.&lt;/p&gt;
&lt;p&gt;解码时则是用户将需要解码的数据放入 &lt;code&gt;SpeexBits&lt;/code&gt;, 将输出数据的缓冲区传给解码函数, 解码函数从中读取, 解码数据, 然后将解码后的数据写入用户指定的缓冲区中.&lt;/p&gt;
&lt;p&gt;另外, SpeexBits 之所以叫 Bits, 是因为它其中存储的数据, 基本单位是比特. 在编码时, 你可能会得到 69.5 个字节. 也就是 556 个比特. 在这种情况下, 我们要存储它的数据时, 肯定是要向上舍入, 也就是存储它内部的 70 个字节.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;帧大小与采样率&lt;/h2&gt;
&lt;p&gt;Speex 的编解码是对于 &quot;帧&quot; 而言的. 每一次编码, 都必须是一个完整的帧, 即一定数量的采样数. 而帧的大小取决于上面提到的 Speex 编码模式.&lt;/p&gt;
&lt;p&gt;而且, 在编码的时候, 传入采样的采样率也应该与编码器设定的采样率一致. 这样才能获得最好的编码效果.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;数据 \ 模式&lt;/th&gt;
&lt;th&gt;窄带&lt;/th&gt;
&lt;th&gt;宽带&lt;/th&gt;
&lt;th&gt;超宽带&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;帧大小&lt;/td&gt;
&lt;td&gt;160&lt;/td&gt;
&lt;td&gt;320&lt;/td&gt;
&lt;td&gt;640&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;默认采样率&lt;/td&gt;
&lt;td&gt;8000Hz&lt;/td&gt;
&lt;td&gt;16000Hz&lt;/td&gt;
&lt;td&gt;32000Hz&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;注意, 帧大小是针对 &apos;采样数量&apos; 的, 例如, 如果你要以宽带模式编码浮点数采样, 那么你需要 320 个浮点采样, 每个 4 字节, 总共需要 1280 个字节. 如果是带符号 16 位整数则是需要 640 个字节.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果你希望将使用 Speex 编码的语音存储到文件, 你可能需要做一些处理. 因为 Speex 的编解码是针对于帧的, 所以你的文件中至少需要有标识帧的地方. 在解码时, 读取一帧, 然后调用解码方法, 得到原始采样.&lt;/p&gt;
&lt;p&gt;最简单的方式就是在每一帧的前面加一字节的头, 这个字节用来标识后面多少字节是一帧.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;编码过程&lt;/h2&gt;
&lt;p&gt;要进行完整的编码, 需要进行以下大概步骤&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;准备用于存储的 SpeexBits&lt;/li&gt;
&lt;li&gt;初始化一个编码器&lt;/li&gt;
&lt;li&gt;调用编码方法&lt;/li&gt;
&lt;li&gt;从 SpeexBits 中读取编码结果&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面是宽带模式编码的示例代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 要进行编码的采样数据
float[] samples;

// 取 320 个采样, 也就是宽带模式下的一帧
float[] frame = samples.Take(320).ToArray();

// 定义并初始化用于存储的 SpeexBits
SpeexBits bits;
Speex.BitsInit(&amp;amp;bits);

// 获取表示宽带模式的指针, 0, 1, 2 分别是窄带, 宽带, 超宽带
SpeexMode* mode = Speex.LibGetMode(1);

// 初始化编码器, 得到表示编码器状态的指针
void* encoderState = Speex.EncoderInit(mode);

// 将数组转为指针
fixed (float* framePtr = frame)
{
    // 重置 SpeexBits 内容
    Speex.BitsReset(&amp;amp;bits);

    // 调用编码方法
    Speex.Encode(encoderState, framePtr, &amp;amp;bits);
}

// 获取编码后的数量 (也就是 bits 中存储的字节数)
int bitCount = bits.BitCount;
int byteCount = (bits.BitCount + 7) &amp;gt;&amp;gt; 3   // 向上舍入

// 声明一个缓冲区用于存储编码后结果
byte[] buffer = new byte[byteCount];

// 固定缓冲区, 转为指针
fixed (byte* bufferPtr = buffer)
{
    // 将 Bits 内存储的编码结果写入到我们自己的缓冲区中
    Speex.BitsWrite(&amp;amp;bits, bufferPtr, buffer.Length);
}

// 做其他处理.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是, 每一次编码之后, 你都应该重置一下 SpeexBits&lt;/p&gt;
&lt;p&gt;因为编码方法的结果再往 SpeexBits 存入时, 如果没有抹除旧的数据, SpeexBits 中就会同时存储着旧的数据和新的数据, 如果你没有手动往 SpeexBits 里面写入一些东西做标识, 那么你就无法区分不同的帧了.&lt;/p&gt;
&lt;p&gt;最简单的方式就是, 每一次编码后, 读取编码结果, 然后清空 SpeexBits.&lt;/p&gt;
&lt;p&gt;如果你需要将所有采样都编码了, 很简单, 只需要用 for 进行循环就好了. 但在这之前, 你还需要对原始的采样做填充处理, 确保它的大小是帧大小的整数倍, 这样你在做编码的时候, 就不会出现访问冲突的问题了.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float[] samples;

int padLength = samples.Length % 360;
if (padLength != 0)
    padLength = 360 - padLength;

float[] paddedSamples = new [samples.Length + padLength];
Array.Copy(samples, 0, paddedSamples, 0, samples.Length);

fixed (float* paddedSamplesPtr = paddedSamples)
{
    for (int i = 0; i &amp;lt; paddedSamples.Length; i += 360)
    {
        Speex.BitsReset(&amp;amp;bits);
        Speex.Encode(encoderState, paddedSamplesPtr + i, &amp;amp;bits);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;解码过程&lt;/h2&gt;
&lt;p&gt;解码同样很简单, 只需要我们将已经编码的一帧以及输出缓冲区传入到 &lt;code&gt;Decode&lt;/code&gt; 函数中, Speex 就会将解码后的一帧存入到缓冲区中.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;准备用于存储的 SpeexBits&lt;/li&gt;
&lt;li&gt;初始化一个解码器&lt;/li&gt;
&lt;li&gt;将需要解码的帧存入到 SpeexBits 中&lt;/li&gt;
&lt;li&gt;调用解码方法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;同样的, 解码的时候也是逐帧解码的, 你传入的缓冲区至少能容纳一帧的音频才可以.&lt;/p&gt;
&lt;p&gt;下面是宽带模式解码的示例代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 要进行解码的 Speex 数据
byte[] speex;

// 声明用于存储解码结果的缓冲区
float[] buffer = new float[320];

// 定义并初始化用于存储的 SpeexBits
SpeexBits bits;
Speex.BitsInit(&amp;amp;bits);

// 获取表示宽带模式的指针
SpeexMode* mode = Speex.LibGetMode(1);

// 初始化解码器
void* decoderState = Speex.DecoderInit(mode);

// 固定 Speex 数据
fixed (byte* speexPtr = speex)
{
    // 将其读入到 SpeexBits 中
    Speex.BitsReadFrom(&amp;amp;bits, speexPtr, speex.Length);
}

// 固定缓冲区
fixed (float* bufferPtr = buffer)
{
    // 进行解码
    Speex.Decode(decoderState, &amp;amp;bits, bufferPtr);
}

// 做其他处理
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是, 在解码时, 读入 SpeexBits 的数据只能是一帧, 如果你存入两帧或者更多的话, 那么解码会出问题.&lt;/p&gt;
&lt;p&gt;如果你存入了半个帧或者数据损坏的一个帧, 解码仍然能成功, 只不过输出结果的质量会下降.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;托管调用&lt;/h2&gt;
&lt;p&gt;以上, 我们已经了解了 Speex 编解码的具体过程, 但 SpeexSharp 还提供了编解码器的类封装, 内部会自动初始化 SpeexBits, 存取编解码数据等.&lt;/p&gt;
&lt;p&gt;如果要使用这些封装好的类来进行编解码, 只需要新建 SpeexEncoder 或 SpeexDecoder 的示例即可.&lt;/p&gt;
</content:encoded></item><item><title>简述数据存储的大小端序</title><link>https://slimenull.com/posts/20231112201146/</link><guid isPermaLink="true">https://slimenull.com/posts/20231112201146/</guid><description>低位字节存储在低位地址, 则是小端序, 低位字节存储在高位地址, 则是大端序</description><pubDate>Sun, 12 Nov 2023 20:11:46 GMT</pubDate><content:encoded>&lt;p&gt;例如一个整数, 6, 它的十六进制表示为: 0x00000006&lt;/p&gt;
&lt;p&gt;共四字节.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;位&lt;/th&gt;
&lt;th&gt;高位&lt;/th&gt;
&lt;th&gt;x&lt;/th&gt;
&lt;th&gt;x&lt;/th&gt;
&lt;th&gt;低位&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;值&lt;/td&gt;
&lt;td&gt;0x00&lt;/td&gt;
&lt;td&gt;0x00&lt;/td&gt;
&lt;td&gt;0x00&lt;/td&gt;
&lt;td&gt;0x06&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;例如我的存储缓冲区有四个字节:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;地址&lt;/th&gt;
&lt;th&gt;0&lt;/th&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;2&lt;/th&gt;
&lt;th&gt;3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;值&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果我将这个数字存入时, 低位字节在低位地址, 则是小端序. 即,&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;地址&lt;/th&gt;
&lt;th&gt;0&lt;/th&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;2&lt;/th&gt;
&lt;th&gt;3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;值&lt;/td&gt;
&lt;td&gt;0x06&lt;/td&gt;
&lt;td&gt;0x00&lt;/td&gt;
&lt;td&gt;0x00&lt;/td&gt;
&lt;td&gt;0x00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果低位字节存储在高位地址, 则是大端序.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;地址&lt;/th&gt;
&lt;th&gt;0&lt;/th&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;2&lt;/th&gt;
&lt;th&gt;3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;值&lt;/td&gt;
&lt;td&gt;0x00&lt;/td&gt;
&lt;td&gt;0x00&lt;/td&gt;
&lt;td&gt;0x00&lt;/td&gt;
&lt;td&gt;0x06&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;在进行网络传输时, 发送者与接收者所使用的数字解析方式必须是使用相同的端序, 否则就会产生数值错误.&lt;/p&gt;
&lt;p&gt;虽然网络通信建议使用大端传输, 但是只要保证发送者和接收者端序一致, 就不会引发错误. 反而如果发送前都做处理, 你的逻辑会是这样, 会很麻烦, 有种脱裤子放屁的感觉:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;BitConverter: 得到小端的字节&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;转为大端序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进行网络传输&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从大端序转为小端序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BitConverter: 得到原始数字&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因为目前常用计算机(x86架构处理器)均为小端序, 并且小端序在寻址时也更加节省性能, 不需要进行额外的计算.&lt;/p&gt;
</content:encoded></item><item><title>[Unity] 个人编码规范与命名准则参考</title><link>https://slimenull.com/posts/20231031081556/</link><guid isPermaLink="true">https://slimenull.com/posts/20231031081556/</guid><description>Unity C# 在写的过程中, 和纯 C# 是有很大出入的. 甚至说, Unity C# 就是邪教. 例如它的命名规范与 C# 是不一致的, 而且由于游戏引擎的介入, 编写时的习惯相较于 C# 来讲, 也需要有所改变.</description><pubDate>Tue, 31 Oct 2023 08:15:56 GMT</pubDate><content:encoded>&lt;p&gt;Unity C# 在写的过程中, 和纯 C# 是有很大出入的. 甚至说, Unity C# 就是邪教. 例如它的命名规范与 C# 是不一致的, 而且由于游戏引擎的介入, 编写时的习惯相较于 C# 来讲, 也需要有所改变.&lt;/p&gt;
&lt;h2&gt;通用编码规范&lt;/h2&gt;
&lt;p&gt;常见的一些编码规范就不需要过多提及了, 这里只做简单列举.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;添加合适的空格和换行, 以使代码变得清晰明确&lt;/li&gt;
&lt;li&gt;相同功能或者相同类型的成员尽量放在一起, 例如私有成员的定义写在一起, 公开成员的定义写在一起.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;命名准则参考&lt;/h2&gt;
&lt;p&gt;关于 &quot;不使用&quot; 的规则&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C# &lt;strong&gt;从来不会&lt;/strong&gt;使用&lt;em&gt;蛇形命名法&lt;/em&gt;, 除了前缀, &lt;strong&gt;不要&lt;/strong&gt;在你的命名中出现下划线. 这样的名称是&lt;strong&gt;不规范&lt;/strong&gt;的: &lt;code&gt;button_ok&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;C# &lt;strong&gt;从来不会&lt;/strong&gt;使用&lt;em&gt;匈牙利命名法&lt;/em&gt;, 也就是在名称的尾部加上用以标识类型的后缀. 这样的名称是&lt;strong&gt;不规范&lt;/strong&gt;的: &lt;code&gt;frmShop&lt;/code&gt;(表示一个商店的窗体)&lt;/li&gt;
&lt;li&gt;C# &lt;strong&gt;基本不会&lt;/strong&gt;使用缩写, 除了常用与通用的缩写例如 &quot;HTTP&quot;(Hyper text transfer protocol) 和 &quot;ID&quot;(Identifier) , 不要使用缩写. 这样的名称是&lt;strong&gt;不规范&lt;/strong&gt;的: &lt;code&gt;acc&lt;/code&gt;(account), &lt;code&gt;btn&lt;/code&gt;(button). 你&lt;strong&gt;应该使用&lt;/strong&gt;它们的&lt;strong&gt;全称&lt;/strong&gt;, 除非它的全称真的很长.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;参考: &lt;a href=&quot;https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/general-naming-conventions&quot;&gt;通用命名约定 - 框架设计指引 | Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;关于 &quot;Unity C# 与 C# 不同之处&quot;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Unity C# 标准库的属性(Properties)并没有按照 C# 规范使用大驼峰命名法.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;成员命名:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有公开的成员都应该使用&lt;em&gt;帕斯卡命名法&lt;/em&gt;.
但是对于属性和字段, 你&lt;em&gt;可以考虑&lt;/em&gt;使用&lt;em&gt;驼峰命名法&lt;/em&gt;, 因为 Unity 就是这么做的. 这个可以根据你的习惯选择.&lt;/li&gt;
&lt;li&gt;私有字段成员建议使用&lt;em&gt;下划线前缀&lt;/em&gt;与&lt;em&gt;驼峰命名法&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;所有的方法名称都应该使用&lt;em&gt;帕斯卡命名法&lt;/em&gt;.
不论其访问等级是怎样的, 方法都应该使用帕斯卡命名法&lt;/li&gt;
&lt;li&gt;所有的方法参数都应该使用&lt;em&gt;驼峰命名法&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类型定义:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有的类型定义都应该使用&lt;strong&gt;帕斯卡命名法&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;接口定义应该有 &apos;I&apos; 前缀.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于语义上的建议.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关于游戏对象的控制脚本, 可以以 &apos;Controller&apos; 为后缀命名, 当然, 如果觉得太长, 使用 &apos;Ctrl&apos; 作为后缀也是可以的.&lt;/li&gt;
&lt;li&gt;当脚本中需要引用某些游戏对象的时候, 可以将这个字段名添加 &apos;Ref&apos; 后缀来表示这是个游戏对象引用.&lt;/li&gt;
&lt;li&gt;当引用的某个游戏对象是专门用来作为另一个游戏对象的父对象的时候, 也就是 &quot;插槽&quot;, 你也可以使用 &apos;Slot&apos; 作为后缀来命名, 这样看到名字就能知道用途.&lt;/li&gt;
&lt;li&gt;当引用一个预制体的时候, 可以使用 &apos;Prefab&apos; 后缀来表示这里需要一个预制体.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么前面说不使用类型前缀, 而这里又推荐使用后缀. 是因为 Unity 中游戏对象可能是场景中的, 也有可能是预制体. 引用的时候, 字段声明是一模一样的. 只能在名称上加后缀加以区分.&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>[.NET] WPF 数据绑定与 MVVM 模型</title><link>https://slimenull.com/posts/20230902090054/</link><guid isPermaLink="true">https://slimenull.com/posts/20230902090054/</guid><pubDate>Sat, 02 Sep 2023 09:00:54 GMT</pubDate><content:encoded>&lt;p&gt;欸? 你还在使用 Dispatcher 来设置与获取数据? 落后啦! 快来试试 MVVM 吧!&lt;/p&gt;
&lt;h2&gt;数据绑定:&lt;/h2&gt;
&lt;p&gt;数据绑定需要有一个源, 这个源可以是一个控件, 也可以是一个对象.&lt;/p&gt;
&lt;p&gt;最简单的数据绑定是对控件属性的绑定. 例如, 一个 Label 始终显示 Slider 的值:
&lt;img src=&quot;/images/20210216034234250.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;/images/4qFhAxjnDcHfmOv.gif&quot; alt=&quot;&quot; /&gt;
绑定数据到某个控件, 此时, 数据绑定的源就是这个控件. Path是Value, 即, 我们要获取与设置这个控件的Value&lt;/p&gt;
&lt;h2&gt;DataContext:&lt;/h2&gt;
&lt;p&gt;如果要设置源为一个对象. 例如我们定义的 MyObj 的实例, 则需要指定 DataContext. 指定 DataContext 的控件以及它的子元素都将使用指定的对象作为数据绑定的源. 例如:
&lt;img src=&quot;/images/2021021604063641.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;/images/20210216040836373.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;MVVM:&lt;/h2&gt;
&lt;p&gt;MVVM 的意思是: Model, View, ViewModel, 其中:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Model: 程序的核心代码, 例如数据库操作&lt;/li&gt;
&lt;li&gt;View: 程序的展示层, 例如 UI 展示与 UI 事件.&lt;/li&gt;
&lt;li&gt;ViewModel: 视图数据层.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;啊我知道你很迷惑... 毕竟当时我也是这样, 反正, 一个例子足够描述出来了.&lt;/p&gt;
&lt;h2&gt;例子:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;新建一个 WPF 项目. 此次演示, 我们以 MvvmTest 作为项目名.&lt;/li&gt;
&lt;li&gt;在项目中创建以下文件夹: Model, View, ViewModel.&lt;/li&gt;
&lt;li&gt;将 MainWindow.xaml 移动到 View 中. 并调整 xaml 中的 Window 的 x:Class 属性值.&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>[.NET/WPF] 设置按钮, 以及其他任何包含边框的控件的圆角</title><link>https://slimenull.com/posts/20230902085924/</link><guid isPermaLink="true">https://slimenull.com/posts/20230902085924/</guid><description>在 WPF 中, 按钮包含一个 “边框”, 很多时候需要设置按钮的圆角, 但是按钮并没有提供一个属性用来设置边框圆角.下面以按钮为例, 列举几种常用的设置圆角的方式.</description><pubDate>Sat, 02 Sep 2023 08:59:24 GMT</pubDate><content:encoded>&lt;p&gt;在 WPF 中, 按钮包含一个 &quot;边框&quot;, 很多时候需要设置按钮的圆角, 但是按钮并没有提供一个属性用来设置边框圆角.&lt;/p&gt;
&lt;p&gt;下面以按钮为例, 列举几种常用的设置圆角的方式.&lt;/p&gt;
&lt;h2&gt;通过附加属性&lt;/h2&gt;
&lt;p&gt;定义一个附加属性, 然后在各个地方就能直接方便的使用了, 下面是实际使用方式:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Button utils.BorderUtils.CornerRadius=&quot;3&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来是具体实现代码, 首先是一些工具方法:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System.Windows.Media;

namespace System.Windows
{
    public static class CommonUtils
    {
        public static void RunOnFirstLoaded(this FrameworkElement element, EventHandler handler)
        {
            void Once(object? sender, RoutedEventArgs e)
            {
                element.Loaded -= Once;
                handler.Invoke(sender, e);
            }

            if (element.IsLoaded)
                handler.Invoke(element, EventArgs.Empty);
            else
                element.Loaded += Once;
        }

        public static TElement? GetElementFromVisualTree&amp;lt;TElement&amp;gt;(this FrameworkElement control) where TElement : FrameworkElement
        {
            if (control is TElement ele)
                return ele;

            int childrenCount = VisualTreeHelper.GetChildrenCount(control);
            for (int i = 0; i &amp;lt; childrenCount; i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(control, i);
                if (child is TElement eleChild)
                    return eleChild;
            }

            return null;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后是 BorderUtils 这个类, 在其中定义 CornerRadius 附加属性:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System;
using System.Windows.Controls;
using System.Windows.Media;

namespace System.Windows
{
    public static class BorderUtils
    {
        [AttachedPropertyBrowsableForType(typeof(FrameworkElement))]
        public static CornerRadius GetCornerRadius(DependencyObject obj)
        {
            return (CornerRadius)obj.GetValue(CornerRadiusProperty);
        }

        public static void SetCornerRadius(DependencyObject obj, CornerRadius value)
        {
            obj.SetValue(CornerRadiusProperty, value);
        }

        // Using a DependencyProperty as the backing store for CornerRadius.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CornerRadiusProperty =
            DependencyProperty.RegisterAttached(&quot;CornerRadius&quot;, typeof(CornerRadius), typeof(BorderUtils), new PropertyMetadata(new CornerRadius(), CornerRadiusChanged));

        private static void CornerRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is not FrameworkElement ele)
                return;

            ele.RunOnFirstLoaded((s, _e) =&amp;gt;
            {
                if (CommonUtils.GetElementFromVisualTree&amp;lt;Border&amp;gt;(ele) is not Border border)
                    return;

                border.CornerRadius = (CornerRadius)e.NewValue;
            });
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;通过资源&lt;/h2&gt;
&lt;p&gt;直接在 Button 节点下添加一个 Border 的样式资源, 然后 Button 中的 Border 就会应用这个样式.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Button&amp;gt;
  &amp;lt;Button.Resources&amp;gt;
    &amp;lt;Style TargetType=&quot;Border&quot;&amp;gt;
      &amp;lt;Setter Property=&quot;CornerRadius&quot; Value=&quot;3&quot; /&amp;gt;
    &amp;lt;/Style&amp;gt;
  &amp;lt;/Button.Resources&amp;gt;
&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;通过模板&lt;/h2&gt;
&lt;p&gt;很麻烦的一种方式, 不推荐&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Button&amp;gt;
  &amp;lt;Button.Template&amp;gt;
    &amp;lt;ControlTemplate TargetType=&quot;{x:Type Button}&quot; &amp;gt;
      &amp;lt;Border BorderBrush=&quot;{TemplateBinding Control.BorderBrush}&quot; BorderThickness=&quot;1&quot; CornerRadius=&quot;7,7,7,7&quot;&amp;gt;
        &amp;lt;Border.Background&amp;gt;#FFDDDDDD&amp;lt;/Border.Background&amp;gt;
        &amp;lt;ContentPresenter Content=&quot;{TemplateBinding ContentControl.Content}&quot; HorizontalAlignment=&quot;Center&quot; VerticalAlignment=&quot;Center&quot; &amp;gt;&amp;lt;/ContentPresenter&amp;gt;
      &amp;lt;/Border&amp;gt;
    &amp;lt;/ControlTemplate&amp;gt;
  &amp;lt;/Button.Template&amp;gt;
&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[Unity, 笔记] 在 Inspector 中显示结构体, 结构体的序列化</title><link>https://slimenull.com/posts/20230831150236/</link><guid isPermaLink="true">https://slimenull.com/posts/20230831150236/</guid><description>给结构体添加 System.Serializable 特性就可以让结构体显示在 Inspector 中了</description><pubDate>Thu, 31 Aug 2023 15:02:36 GMT</pubDate><content:encoded>&lt;p&gt;在 Unity 中的 Inspector, 基本数据类型, 数组, 都是可以直接显示出来的, 但创建出来的结构体不能直接显示出来.&lt;/p&gt;
&lt;p&gt;为结构体添加一个 &lt;code&gt;System.Serializable&lt;/code&gt; 特性即可使其能够显示在 Inspector 中.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Serializable]
struct CustomData
{
    public int SomeInteger;
    public string SomeString;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[Unity] 单例设计模式, 可供继承的单例组件模板类</title><link>https://slimenull.com/posts/20230828202236/</link><guid isPermaLink="true">https://slimenull.com/posts/20230828202236/</guid><description>Unity 单例模板类</description><pubDate>Mon, 28 Aug 2023 20:22:36 GMT</pubDate><content:encoded>&lt;p&gt;一个可供继承的单例组件模板类:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class SingletonComponent&amp;lt;TComponent&amp;gt; : MonoBehavior
    where TComponent : SingletonComponent&amp;lt;TComponent&amp;gt;
{
    static TComponent _instance;

    private static TComponent GetOrFindOrCreateComponent()
    {
       // 双检索
        if (_instance == null)
        {
            // 尝试在场景中查找已存在的组件
            _instance = FindObjectOfType&amp;lt;TComponent&amp;gt;();
            
            // 如果找不到, 则创建一个空对象, 并且挂载上组件
            if (_instance == null)
            {
                GameObject gameObject = new GameObject();
                _instance = gameObject.AddComponent&amp;lt;TComponent&amp;gt;();
            }
        }

        return _instance;
    }

    public static TComponent Instance =&amp;gt; GetOrFindOrCreateComponent();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;因为 Unity 是单线程的, 所以在这里没有必要使用双检索&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;使用方式&lt;/h2&gt;
&lt;p&gt;例如你要创建一个全局的单例管理类, 可以这样使用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class GameManager : SingletonComponent&amp;lt;GameManager&amp;gt;
{
    // your code here
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;p&gt;尽量避免让 &lt;code&gt;SingletonComponent&lt;/code&gt; 帮你创建组件, 因为它只是单纯的将组件创建, 并挂载到空对象上, 而不会进行任何其他行为. 如果你的组件需要进行某些初始化, 那么它可能不会正常.&lt;/p&gt;
</content:encoded></item><item><title>[Unity] 基础的编程思想, 组件式开发</title><link>https://slimenull.com/posts/20230818165542/</link><guid isPermaLink="true">https://slimenull.com/posts/20230818165542/</guid><description>熟悉 C# 开发的朋友, 在刚进入 Unity 开发时, 不可避免的会有一些迷惑, 例如不清楚 Unity 自己的思想, 如何设计与架构一个应用程序之类的. 本篇文章简要的介绍一下 Unity 的基础编程思想.</description><pubDate>Fri, 18 Aug 2023 16:55:42 GMT</pubDate><content:encoded>&lt;p&gt;熟悉 C# 开发的朋友, 在刚进入 Unity 开发时, 不可避免的会有一些迷惑, 例如不清楚 Unity 自己的思想, 如何设计与架构一个应用程序之类的. 本篇文章简要的介绍一下 Unity 的基础编程思想.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;独立&lt;/h2&gt;
&lt;p&gt;Unity 很少使用 C# 的标准库, 例如 C# 的网络, 事件驱动, 对象模型, 这些概念在 Unity 中几乎不会被用到. 甚至于, 连标准的 System 命名空间都很少被引用.&lt;/p&gt;
&lt;p&gt;Unity 自己实现了一套较为完善的标准库, 以适用于游戏开发. 例如最简单的 &quot;随机数&quot;, 在 Unity 中, &lt;code&gt;UnityEngine.Random&lt;/code&gt; 类实现了随机数生成. 并且它是一个静态类, 不需要我们创建随机数对象, 即可使用.&lt;/p&gt;
&lt;p&gt;Unity 用到的, 最多是 C# 中的集合, LINQ 查询这些. 甚至于, Unity 中的 UI 事件处理器, 都不是一个标准的 C# &lt;code&gt;EventHandler&lt;/code&gt;. 完完全全算得上邪教.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;命名&lt;/h2&gt;
&lt;p&gt;刚开始使用 Unity 的你, 肯定会疑惑,,, 为什么 Unity 的脚本中, 用于获取 &lt;code&gt;Transform&lt;/code&gt; 组件的属性, 叫做 &lt;code&gt;transform&lt;/code&gt;, 而不是 &lt;code&gt;Transform&lt;/code&gt;. 一个公开的属性, 却不使用大驼峰命名法, 而是使用小驼峰?&lt;/p&gt;
&lt;p&gt;事实上, Unity 压根不用 C# 的命名规范, 只有一部分与 C# 的命名规范统一. 例如, 它的类型还是大驼峰的, 它的接口也是以 &lt;code&gt;I&lt;/code&gt; 开头的大驼峰命名. 不过单单属性命名与 C# 不统一这件事, 就已经能让大多数注重优雅的 C# 程序员火冒三丈了.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;对象&lt;/h2&gt;
&lt;p&gt;刚刚也提到了, Unity 自己单独实现了一整套标准库, 只有少部分是使用 C# 的东西. 事实上, 在 Unity 中, 所有 Unity 可引用的对象都是由 &lt;code&gt;UnityEngine.Object&lt;/code&gt; 派生而来的. 例如游戏对象 &lt;code&gt;UnityEngine.GameObject&lt;/code&gt;, 还有诸如 &lt;code&gt;Camea&lt;/code&gt;, &lt;code&gt;RigidBody&lt;/code&gt; 这些各种各样的组件.&lt;/p&gt;
&lt;p&gt;Unity 的对象封装了很多静态方法, 例如克隆一个对象, 销毁一个对象, 在场景中查找某个类型的对象. 由于我们在 Unity 中编辑的 C# 代码大多是一个继承了 &lt;code&gt;UnityEngine.MonoBehavior&lt;/code&gt; 的脚本, 而它又间接继承 &lt;code&gt;UnityEngine.Object&lt;/code&gt;, 所以我们在代码中访问这些方法的时候, 就不需要写类名, 直接调用即可.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;游戏对象与组件&lt;/h2&gt;
&lt;p&gt;在 C# 中, 实现功能的拓展是通过继承来实现的, 例如 &lt;code&gt;Button&lt;/code&gt; 继承 &lt;code&gt;Control&lt;/code&gt;, 并在 &lt;code&gt;Control&lt;/code&gt; 的基础上拓展出自己的功能, 但是 Unity 不是这样.&lt;/p&gt;
&lt;p&gt;Unity 中, 每一个场景中的对象, 就单纯只是一个 &lt;code&gt;GameObject&lt;/code&gt;, 它自己本身没有任何功能. 如果要给一个对象添加一些功能, 就要为它挂载一些组件. 例如游戏中原始的一个 &lt;code&gt;Sphere&lt;/code&gt; 物体, 它由 &lt;code&gt;Transform&lt;/code&gt;, &lt;code&gt;MeshFilter&lt;/code&gt;, &lt;code&gt;MeshRenderer&lt;/code&gt;, &lt;code&gt;SphereCollider&lt;/code&gt; 这些组件构成, 他们分别负责承载 &quot;物体的变换, 包括位置, 旋转, 缩放&quot;, &quot;网格的存储&quot;, &quot;网格的渲染&quot;, &quot;碰撞相关功能&quot;.&lt;/p&gt;
&lt;p&gt;不过, Unity 中的组件本身还是存在继承关系的. 例如 &lt;code&gt;SphereCollider&lt;/code&gt; 和 &lt;code&gt;BoxCollider&lt;/code&gt; 都是 &lt;code&gt;Collider&lt;/code&gt; 的派生类.&lt;/p&gt;
&lt;p&gt;只要理解了任何游戏中的物体都有一个最基本的游戏对象和提供功能的组件构成, 就可以理解大部分问题了. 根据这一点, 如果你想自己手动创建一个 &lt;code&gt;Sphere&lt;/code&gt;, 也可以先创建一个空对象, 然后按照 &lt;code&gt;Sphere&lt;/code&gt; 的构成, 将所需要的组件逐个添加上去, 并正确的设置好属性, 就可以完美的复制出和自带的 &lt;code&gt;Sphere&lt;/code&gt; 一模一样的物体了.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;组件之间的依赖与互斥&lt;/h2&gt;
&lt;p&gt;组件也是有相互的关系的, 就像我们所用的 &lt;code&gt;控制反转&lt;/code&gt; 中的服务一样, 我们将功能抽成一个个服务, 而服务之间存在依赖关系. Unity 的组件也是如此.&lt;/p&gt;
&lt;p&gt;如果要使功能正常运行, 就需要同时添加组件所依赖的组件. 例如要实现物理效果, 我们需要添加一个 &lt;code&gt;RigidBody&lt;/code&gt; 组件, 但是要使物体之间能够产生碰撞, 就需要再添加一个 &lt;code&gt;Collider&lt;/code&gt;. 如果单独添加 &lt;code&gt;RigidBody&lt;/code&gt;, 虽然物体会受重力影响, 但是因为不会产生碰撞, 所以会直接穿过其他物体.&lt;/p&gt;
&lt;p&gt;至于组件的互斥, 有两种, 一种是完全不能共存的, 例如无法为一个物体同时添加 &lt;code&gt;BoxCollider&lt;/code&gt; 和 &lt;code&gt;BoxCollider2D&lt;/code&gt;, 如果尝试添加, 编辑器中会提示 &quot;组件冲突&quot;. 另一种则是, 可以同时添加, 但是会产生一些奇怪效果的. 例如 &lt;code&gt;CharacterController&lt;/code&gt; 和 &lt;code&gt;RigidBody&lt;/code&gt; 同时添加到物体中时, 就会引起移动相关的问题.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;全局单例与静态类&lt;/h2&gt;
&lt;p&gt;Unity 中很少使用静态类, 除非你要用静态类承载一些工具方法. 但如果是一些逻辑相关的东西, 那它基本都是像普通的脚本一样是非静态的, 而想要全局访问, 在 Unity 中更多使用的是 &quot;单例模式&quot;.&lt;/p&gt;
&lt;p&gt;在平常的开发中, 我们大抵是很少用到单例的. 因为直接用静态就可以解决问题. 但是 Unity 中使用静态的话, 我们就没办法将它作为组件挂载到游戏对象上, 而且也不能通过 Unity 的编辑器对它的一些变量进行赋值, 不是很方便. 所以 Unity 单例用的更多.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是, 平常编程中所使用的单例是没办法直接应用在 Unity 上的, Unity 的对象产生与销毁都与平常的开发有所不同, 需要做一点针对 Unity 的 &quot;本地化&quot; 才能正确使用.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;管理类&lt;/h2&gt;
&lt;p&gt;Unity 会使用大量的 &quot;管理类&quot; 用来集中管理逻辑. 甚至于, 当你创建一个名字为 &lt;code&gt;GameManager&lt;/code&gt; 的脚本时, 它的图标不是一个 C# 脚本图标, 而是一个特殊的齿轮图标. 由此可见 Unity 开发中有多重视这个.&lt;/p&gt;
&lt;p&gt;相比较我们平常的桌面开发, 后端开发, 之所以 Unity 更常使用所谓的管理类, 是因为 Unity 游戏对象的通信是比较麻烦的, 而且一整个游戏的逻辑也更加复杂, 例如一场战斗的进行, 这时使用一个 &quot;战斗管理&quot; 的类来记录战斗相关的数据, 调用角色进行战斗, 要更加方便些.&lt;/p&gt;
</content:encoded></item><item><title>[.NET/WPF] CommunityToolkit.Mvvm 异步指令</title><link>https://slimenull.com/posts/20230818143649/</link><guid isPermaLink="true">https://slimenull.com/posts/20230818143649/</guid><description>CommunityToolkit.Mvvm 中的 RelayCommand 除了支持最简单的同步方法, 还支持以 Task 作为返回值的异步方法.</description><pubDate>Fri, 18 Aug 2023 14:36:49 GMT</pubDate><content:encoded>&lt;p&gt;我们在开发中, 经常会有这样的需求:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;点击按钮后, 进行一些耗时的工作&lt;/li&gt;
&lt;li&gt;工作进行时, 按钮不可再次被点击&lt;/li&gt;
&lt;li&gt;工作进行时, 会显示进度条, 或者 &quot;加载中&quot; 的动画&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;RelayCommand&lt;/h2&gt;
&lt;p&gt;CommunityToolkit.Mvvm 中的 RelayCommand 除了支持最简单的同步方法, 还支持以 Task 作为返回值的异步方法, 当我们为这样的异步方法标记上 &lt;code&gt;RelayCommand&lt;/code&gt; 特性时, 它会生成一个对应的异步指令.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;指令在执行时, 主要逻辑会在后台, 而不是运行在 UI 线程中. 具体可以参考 &quot;异步和异步的线程切换&quot;&lt;/li&gt;
&lt;li&gt;指令在执行时, CanExecute 会变为 false, 此时使用该 Command 的 Button 或者其他控件, 也会变成 &apos;被禁用&apos; 的状态.&lt;/li&gt;
&lt;li&gt;如果方法参数中包含一个 &lt;code&gt;CancellationToken&lt;/code&gt;, 那么这个任务同样可以被取消. 只需要你方法内部有正确实现 &quot;取消执行&quot; 的逻辑就没问题.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面是一个例子.&lt;/p&gt;
&lt;p&gt;主窗体代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Window x:Class=&quot;LearnMvvm.MainWindow&quot;
        xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
        xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
        xmlns:d=&quot;http://schemas.microsoft.com/expression/blend/2008&quot;
        xmlns:mc=&quot;http://schemas.openxmlformats.org/markup-compatibility/2006&quot;
        xmlns:local=&quot;clr-namespace:LearnMvvm&quot;
        xmlns:vm=&quot;clr-namespace:LearnMvvm.ViewModels&quot;
        mc:Ignorable=&quot;d&quot;
        Title=&quot;MainWindow&quot; Height=&quot;450&quot; Width=&quot;7![请添加图片描述](/images/65ddbab8f917458bbfd40ebdb6bb6d16.gif)
00&quot;&amp;gt;
    &amp;lt;Window.DataContext&amp;gt;
        &amp;lt;vm:MainViewModel/&amp;gt;
    &amp;lt;/Window.DataContext&amp;gt;
    &amp;lt;Grid&amp;gt;
        &amp;lt;StackPanel Margin=&quot;50&quot;&amp;gt;
            &amp;lt;Button Command=&quot;{Binding DoSomethingCommand}&quot;&amp;gt;Do something&amp;lt;/Button&amp;gt;
            &amp;lt;Button Command=&quot;{Binding CancelDoSomethingCommand}&quot; Margin=&quot;0 5 0 0&quot;&amp;gt;Cancel&amp;lt;/Button&amp;gt;

            &amp;lt;ProgressBar Margin=&quot;0 10 0 0&quot; Height=&quot;15&quot; Value=&quot;{Binding Progress}&quot;/&amp;gt;
        &amp;lt;/StackPanel&amp;gt;
    &amp;lt;/Grid&amp;gt;
&amp;lt;/Window&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后台 ViewModel&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    private int progress;

    [RelayCommand]
    public async Task DoSomething(CancellationToken token)
    {
        for (int i = 0; i &amp;lt;= 100; i++)
        {
            if (token.IsCancellationRequested)
                return;

            await Task.Delay(100);
            Progress = i;
        }
    }

    [RelayCommand]
    public void CancelDoSomething()
    {
        DoSomethingCommand.Cancel();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/ca49f2eac6c945d29321a6cc1181787f.gif&quot; alt=&quot;演示&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/ca07eac8f7284e51a4607ec072cadafe.gif&quot; alt=&quot;取消执行&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在 CommunityToolkit.Mvvm 中, 支持以下样式的 RelayCommand 签名:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;void 方法名()&lt;/code&gt; 无参同步指令&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void 方法名(类型 参数名)&lt;/code&gt; 有参同步指令&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Task 方法名()&lt;/code&gt;, 无参, 不支持取消的异步指令&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Task 方法名(类型 参数名)&lt;/code&gt; 有参, 不支持取消的异步指令&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Task 方法名(CancellationToken token)&lt;/code&gt; 无参, 支持取消的异步指令&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Task 方法名(类型 参数名, CancellationToken token)&lt;/code&gt; 有参, 支持取消的异步指令&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>EV 录屏修复小工具</title><link>https://slimenull.com/posts/20230815153735/</link><guid isPermaLink="true">https://slimenull.com/posts/20230815153735/</guid><description>我用 C# 写了一个 EV 录屏修复小工具.</description><pubDate>Tue, 15 Aug 2023 15:37:35 GMT</pubDate><content:encoded>&lt;p&gt;参考这篇文章, &lt;a href=&quot;https://blog.csdn.net/LWD19981223/article/details/124585476&quot;&gt;EV录制文件损坏-修复方法&lt;/a&gt;, 我用 C# 写了一个小程序.&lt;/p&gt;
&lt;p&gt;仓库: &lt;a href=&quot;https://github.com/SlimeNull/EvRepair&quot;&gt;github.com/SlimeNull/EvRepair&lt;/a&gt;
下载: &lt;a href=&quot;https://github.com/SlimeNull/EvRepair/releases&quot;&gt;github.com/SlimeNull/EvRepair/Releases&lt;/a&gt;
镜像: &lt;a href=&quot;https://gitee.com/slimenull/EvRepair/releases&quot;&gt;gitee.com/slimenull/EvRepair/releases&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;觉得还不错的话, 点个星星&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;推荐使用的几个理由:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;内嵌 &lt;code&gt;ffmpeg&lt;/code&gt; 和 &lt;code&gt;recovery_MP4.exe&lt;/code&gt;, 不需要用户自己翻墙下载&lt;/li&gt;
&lt;li&gt;全自动的, 理所当然的, 不需要打指令, 就可以完成操作, 只需要跟着提示走就好了&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;FAQ:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;怎么使用
如果有看不懂的东西, 就直接一路回车使用默认选项就好.&lt;/li&gt;
&lt;li&gt;提示路径不存在
输入路径的时候, 不要带双引号&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>[C#] 简单的俄罗斯方块实现</title><link>https://slimenull.com/posts/20230809093633/</link><guid isPermaLink="true">https://slimenull.com/posts/20230809093633/</guid><description>简单控制台俄罗斯方块实现</description><pubDate>Wed, 09 Aug 2023 09:36:33 GMT</pubDate><content:encoded>&lt;p&gt;一个控制台俄罗斯方块游戏的简单实现. 已在 &lt;a href=&quot;https://github.com/SlimeNull/Tetris&quot;&gt;github.com/SlimeNull/Tetris&lt;/a&gt; 开源.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/dd0c115c985046c6a9267bab3288cc8e.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;思路&lt;/h2&gt;
&lt;p&gt;很简单, 一个二维数组存储当前游戏的方块地图, 用 &lt;code&gt;bool&lt;/code&gt; 即可, &lt;code&gt;true&lt;/code&gt; 表示当前块被填充, &lt;code&gt;false&lt;/code&gt; 表示没有.&lt;/p&gt;
&lt;p&gt;然后, 抽一个 &quot;形状&quot; 类, 形状表示当前玩家正在操作的一个形状, 例如方块, 直线, T 形什么的. 一个形状又有不同的样式, 也就是玩家可以切换的样式. 每一个样式都是原来样式旋转之后的结果. 为了方便, 可以直接使用硬编码的方式存储所有样式中方块的相对坐标.&lt;/p&gt;
&lt;p&gt;一个形状有一个自己的坐标, 并且它包含很多方块. 在绘制的时候, 获取它每一个方块的坐标, 转换为地图内的绝对坐标, 然后使用 &lt;code&gt;StringBuilder&lt;/code&gt; 拼接字符串, 即可.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;资料&lt;/h2&gt;
&lt;p&gt;俄罗斯方块中总共有这七种方块&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=&quot;center&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/7352d9c5ae3f4f9f8f1bea49a719dae4.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;类型定义&lt;/h2&gt;
&lt;p&gt;一个简单的二维坐标&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// 表示一个坐标
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;param name=&quot;X&quot;&amp;gt;&amp;lt;/param&amp;gt;
/// &amp;lt;param name=&quot;Y&quot;&amp;gt;&amp;lt;/param&amp;gt;
record struct Coordinate(int X, int Y)
{
    /// &amp;lt;summary&amp;gt;
    /// 根据基坐标和相对坐标, 获取一个绝对坐标
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;baseCoord&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;relativeCoord&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public static Coordinate GetAbstract(Coordinate baseCoord, Coordinate relativeCoord)
    {
        return new Coordinate(baseCoord.X + relativeCoord.X, baseCoord.Y + relativeCoord.Y);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;形状的一个样式, 单纯使用坐标数组存储即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;record struct ShapeStyle(Coordinate[] Coordinates);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;形状&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// 形状基类
/// &amp;lt;/summary&amp;gt;
abstract class Shape
{
    /// &amp;lt;summary&amp;gt;
    /// 名称
    /// &amp;lt;/summary&amp;gt;
    public abstract string Name { get; }

    /// &amp;lt;summary&amp;gt;
    /// 形状的位置
    /// &amp;lt;/summary&amp;gt;
    public Coordinate Position { get; set; }

    /// &amp;lt;summary&amp;gt;
    /// 形状所有的样式
    /// &amp;lt;/summary&amp;gt;
    protected abstract ShapeStyle[] ShapeStyles { get; }

    /// &amp;lt;summary&amp;gt;
    /// 当前使用的样式索引
    /// &amp;lt;/summary&amp;gt;
    private int _currentStyleIndex = 0;

    /// &amp;lt;summary&amp;gt;
    /// 从坐标构建一个新形状
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;position&quot;&amp;gt;&amp;lt;/param&amp;gt;
    public Shape(Coordinate position)
    {
        Position = position;
    }

    /// &amp;lt;summary&amp;gt;
    /// 获取当前形状的当前所有方块 (相对坐标)
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public IEnumerable&amp;lt;Coordinate&amp;gt; GetBlocks()
    {
        return ShapeStyles[_currentStyleIndex].Coordinates;
    }

    /// &amp;lt;summary&amp;gt;
    /// 获取当前形状下一个样式的所有方块 (相对坐标)
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public IEnumerable&amp;lt;Coordinate&amp;gt; GetNextStyleBlocks()
    {
        return ShapeStyles[(_currentStyleIndex + 1) % ShapeStyles.Length].Coordinates;
    }

    /// &amp;lt;summary&amp;gt;
    /// 改变样式
    /// &amp;lt;/summary&amp;gt;
    public void ChangeStyle()
    {
        _currentStyleIndex = (_currentStyleIndex + 1) % ShapeStyles.Length;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个 T 形状的实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ShapeT : Shape
{
    public ShapeT(Coordinate position) : base(position)
    {
    }

    public override string Name =&amp;gt; &quot;T&quot;;

    protected override ShapeStyle[] ShapeStyles { get; } = new ShapeStyle[]
    {
        new ShapeStyle(
            new Coordinate[]
            {
                new Coordinate(-1, 0),
                new Coordinate(0, 0),
                new Coordinate(1, 0),
                new Coordinate(0, 1),
            }),
        new ShapeStyle(
            new Coordinate[]
            {
                new Coordinate(-1, 0),
                new Coordinate(0, -1),
                new Coordinate(0, 0),
                new Coordinate(0, 1),
            }),
        new ShapeStyle(
            new Coordinate[]
            {
                new Coordinate(-1, 0),
                new Coordinate(0, 0),
                new Coordinate(1, 0),
                new Coordinate(0, -1),
            }),
        new ShapeStyle(
            new Coordinate[]
            {
                new Coordinate(1, 0),
                new Coordinate(0, -1),
                new Coordinate(0, 0),
                new Coordinate(0, 1),
            }),
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;主逻辑&lt;/h2&gt;
&lt;p&gt;上面的定义已经写好了, 接下来就是写游戏主逻辑.&lt;/p&gt;
&lt;p&gt;主逻辑包含每一回合自动向下移动形状, 如果无法继续向下移动, 则把当前的形状存储到地图中. 并进行一次扫描, 将所有的整行全部消除.&lt;/p&gt;
&lt;p&gt;抽一个 &lt;code&gt;TetrisGame&lt;/code&gt; 的类用来表示俄罗斯方块游戏, 下面是这个类的基本定义.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class TetrisGame
{
    /// &amp;lt;summary&amp;gt;
    /// x, y
    /// &amp;lt;/summary&amp;gt;
    private readonly bool[,] map;

    private readonly Random random = new Random();


    public TetrisGame(int width, int height)
    {
        map = new bool[width, height];

        Width = width;
        Height = height;
    }

    public Shape? CurrentShape { get; set; }

    public int Width { get; }
    public int Height { get; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;判断当前形状是否可以进行移动的方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// 判断是否可以移动 (移动后是否会与已有方块重合, 或者超出边界)
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;param name=&quot;xOffset&quot;&amp;gt;&amp;lt;/param&amp;gt;
/// &amp;lt;param name=&quot;yOffset&quot;&amp;gt;&amp;lt;/param&amp;gt;
/// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
private bool CanMove(int xOffset, int yOffset)
{
    // 如果当前没形状, 返回 false
    if (CurrentShape == null)
        return false;

    foreach (var block in CurrentShape.GetBlocks())
    {
        Coordinate coord = Coordinate.GetAbstract(CurrentShape.Position, block);

        coord.X += xOffset;
        coord.Y += yOffset;

        // 如果移动后方块坐标超出界限, 不能移动
        if (coord.X &amp;lt; 0 || coord.X &amp;gt;= Width ||
            coord.Y &amp;lt; 0 || coord.Y &amp;gt;= Height)
            return false;

        // 如果移动后方块会与地图现有方块重合, 则不能移动
        if (map[coord.X, coord.Y])
            return false;
    }

    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;判断当前形状是否能够切换到下一个样式的方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// 判断是否可以改变形状 (改变形状后是否会和已有方块重合, 或者超出边界)
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
private bool CanChangeShape()
{
    // 如果当前没形状, 当然不能切换样式
    if (CurrentShape == null)
        return false;

    // 获取下一个样式的所有方块
    foreach (var block in CurrentShape.GetNextStyleBlocks())
    {
        Coordinate coord = Coordinate.GetAbstract(CurrentShape.Position, block);

        // 如果超出界限, 不能切换
        if (coord.X &amp;lt; 0 || coord.X &amp;gt;= Width ||
            coord.Y &amp;lt; 0 || coord.Y &amp;gt;= Height)
            return false;

        // 如果与现有方块重合, 不能切换
        if (map[coord.X, coord.Y])
            return false;
    }

    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把当前形状存储到地图中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// 将当前形状存储到地图中
/// &amp;lt;/summary&amp;gt;
private void StorageShapeToMap()
{
    // 没形状, 存寂寞
    if (CurrentShape == null)
        return;

    // 所有方块遍历一下
    foreach (var block in CurrentShape.GetBlocks())
    {
        // 转为绝对坐标
        Coordinate coord = Coordinate.GetAbstract(CurrentShape.Position, block);

        // 超出界限则跳过
        if (coord.X &amp;lt; 0 || coord.X &amp;gt;= Width ||
            coord.Y &amp;lt; 0 || coord.Y &amp;gt;= Height)
            continue;

        // 存地图里
        map[coord.X, coord.Y] = true;
    }

    // 当前形状设为 null
    CurrentShape = null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成一个新形状&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// 生成一个新形状
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;exception cref=&quot;InvalidOperationException&quot;&amp;gt;&amp;lt;/exception&amp;gt;
private void GenerateShape()
{
    int shapeCount = 7;
    int randint = random.Next(shapeCount);

    Coordinate initCoord = new Coordinate(Width / 2, 0);

    Shape newShape = randint switch
    {
        0 =&amp;gt; new ShapeI(initCoord),
        1 =&amp;gt; new ShapeJ(initCoord),
        2 =&amp;gt; new ShapeL(initCoord),
        3 =&amp;gt; new ShapeO(initCoord),
        4 =&amp;gt; new ShapeS(initCoord),
        5 =&amp;gt; new ShapeT(initCoord),
        6 =&amp;gt; new ShapeZ(initCoord),

        _ =&amp;gt; throw new InvalidOperationException()
    };

    CurrentShape = newShape;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;扫描地图, 消除所有整行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// 扫描, 消除掉可消除的行
/// &amp;lt;/summary&amp;gt;
private void Scan()
{
    for (int y = 0;  y &amp;lt; Height; y++)
    {
        // 设置当前行是整行
        bool ok = true;

        // 循环当前行的所有方块, 如果方块为 false, ok 就会被设为 false
        for (int x = 0; x &amp;lt; Width; x++)
            ok &amp;amp;= map[x, y];

        // 如果当前行确实是整行
        if (ok)
        {
            // 所有行全部往下移动
            for (int _y = y; _y &amp;gt; 0; _y--)
                for (int x = 0; x &amp;lt; Width; x++)
                    map[x, _y] = map[x, _y - 1];
            
            // 最顶行全设为空
            for (int x = 0; x &amp;lt; Width; x++)
                map[x, 0] = false;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;封装一些用户操作使用的方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// 根据指定偏移, 进行移动
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;param name=&quot;xOffset&quot;&amp;gt;&amp;lt;/param&amp;gt;
/// &amp;lt;param name=&quot;yOffset&quot;&amp;gt;&amp;lt;/param&amp;gt;
public void Move(int xOffset, int yOffse
{
    lock (this)
    {
        if (CurrentShape == null)
            return;

        if (CanMove(xOffset, yOffset))
        {
            var newCoord = CurrentShape.
            newCoord.X += xOffset;
            newCoord.Y += yOffset;

            CurrentShape.Position = newC
        }
    }
}

/// &amp;lt;summary&amp;gt;
/// 向左移动
/// &amp;lt;/summary&amp;gt;
public void MoveLeft()
{
    Move(-1, 0);
}

/// &amp;lt;summary&amp;gt;
/// 向右移动
/// &amp;lt;/summary&amp;gt;
public void MoveRight()
{
    Move(1, 0);
}

/// &amp;lt;summary&amp;gt;
/// 向下移动
/// &amp;lt;/summary&amp;gt;
public void MoveDown()
{
    Move(0, 1);
}

/// &amp;lt;summary&amp;gt;
/// 改变形状样式
/// &amp;lt;/summary&amp;gt;
public void ChangeShapeStyle()
{
    lock (this)
    {
        if (CurrentShape == null)
            return;

        if (CanChangeShape())
            CurrentShape.ChangeStyle();
    }
}

/// &amp;lt;summary&amp;gt;
/// 降落到底部
/// &amp;lt;/summary&amp;gt;
public void Fall()
{
    lock (this)
    {
        while (CanMove(0, 1))
        {
            Move(0, 1);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;游戏每一轮的主逻辑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// 下一个回合
/// &amp;lt;/summary&amp;gt;
public void NextTurn()
{
    lock (this)
    {
        // 如果当前没有存在的形状, 则生成一个新的, 并返回
        if (CurrentShape == null)
        {
            GenerateShape();
            return;
        }

        // 如果可以向下移动
        if (CanMove(0, 1))
        {
            // 直接改变当前形状的坐标
            var newCoord = CurrentShape.Position;
            newCoord.Y += 1;

            CurrentShape.Position = newCoord;
        }
        else
        {
            // 将当前的形状保存到地图中
            StorageShapeToMap();
        }

        // 扫描, 判断某些行可以被消除
        Scan();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将地图渲染到控制台&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void Render()
{
    StringBuilder sb = new StringBuilder();

    bool[,] mapCpy = new bool[Width, Height];
    Array.Copy(map, mapCpy, mapCpy.Length);

    if (CurrentShape != null)
    {
        foreach (var block in CurrentShape.GetBlocks())
        {
            Coordinate coord = Coordinate.GetAbstract(CurrentShape.Position, block);
            if (coord.X &amp;lt; 0 || coord.X &amp;gt;= Width ||
                coord.Y &amp;lt; 0 || coord.Y &amp;gt;= Height)
                continue;

            mapCpy[coord.X, coord.Y] = true;
        }
    }

    sb.AppendLine(&quot;┌&quot; + new string(&apos;─&apos;, Width * 2) + &quot;┐&quot;);
    for (int y = 0; y &amp;lt; Height; y++)
    {
        sb.Append(&quot;|&quot;);

        for (int x = 0; x &amp;lt; Width; x++)
        {
            sb.Append(mapCpy[x, y] ? &quot;##&quot; : &quot;  &quot;);
        }

        sb.Append(&quot;|&quot;);

        sb.AppendLine();
    }

    sb.AppendLine(&quot;└&quot; + new string(&apos;─&apos;, Width * 2) + &quot;┘&quot;);

    lock (this)
    {
        Console.SetCursorPosition(0, 0);
        Console.Write(sb.ToString());
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[刷机] 9008 刷机救砖笔记</title><link>https://slimenull.com/posts/20230719163623/</link><guid isPermaLink="true">https://slimenull.com/posts/20230719163623/</guid><description>9008 模式刷机并不难, 学会之后, 就再也不用害怕手机成砖了</description><pubDate>Wed, 19 Jul 2023 16:36:23 GMT</pubDate><content:encoded>&lt;p&gt;前几日, 我拿着手里的 OnePlus 8T 作死, 成功的把它作成了砖, 系统分区嗝屁, recovery 和 bootloader 分区也都嗝屁, 换言之, 只能使用 9008 进行刷机了...&lt;/p&gt;
&lt;h2&gt;什么是 9008 深刷&lt;/h2&gt;
&lt;p&gt;按照网上的介绍, 9008 是骁龙系列处理器的手机专有的, 用于为手机硬件下载数据的模式. 只要手机还能通电, 那 9008 模式就能用. 所以, 无论手机怎么成砖了, 都能通过这种方式, 为手机重新刷入系统, 达到救砖的目的.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里说的 &quot;下载数据&quot; 指的是向手机存储器写入数据&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;有什么要求&lt;/h2&gt;
&lt;p&gt;因为 9008 模式是骁龙处理器独有的功能, 所以, 你的手机必须搭载骁龙处理器. 联发科或者其他则不可以.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不过据我所知, 联发科也有和 9008 对标的一个救砖方式, 只不过我没有试过.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;需要做什么准备&lt;/h2&gt;
&lt;p&gt;你电脑需要有骁龙的 USB 驱动, 这样, 在电脑连接到手机的时候, 电脑才能识别到手机.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一般的, 这个驱动都比较难找, 例如太新的版本可能不行, 太旧的版本也可能不行, 在 Windows11 上, 驱动还需要签名, 或者需要手动添加测试证书什么的, 不过, 只需要去拿着手机牌子, 在网上去搜索驱动下载, 总是能下载到合适的驱动的.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在有驱动之后, 你电脑上还需要有专用的 9008 救砖包, 一般厂商会提供这样的救砖包, 一个救砖包中包含刷机工具, 以及刷机包. 在找刷机包的时候, 只需要拿着手机的具体型号, 去搜索即可.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果官网找不到的话, 可以去一些刷机论坛上找找, 使用魔法上网, 去 XDA 上能找到许多的你想知道的东西, 例如一些刷机教程.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;如何进行操作&lt;/h2&gt;
&lt;p&gt;对于开放性最强的一加手机, 想要进入 9008 模式, 操作起来极其简单!&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;手机处于关机状态, 并同时按下音量上下键&lt;/li&gt;
&lt;li&gt;使用数据线连接电脑与手机&lt;/li&gt;
&lt;li&gt;电脑的设备管理器中, 出现了一个新的串口设备, 名字是 &quot;Qualcomm HS-USB QDLoader 9008 (COMx)&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;FAQ:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果你电脑没有识别到任何设备
请确认, 你的线是否是数据线, 你是否按住了音量上下键, 以及你手机是否支持通过这种方式进入 9008 模式&lt;/li&gt;
&lt;li&gt;电脑识别到了一个新设备, 但它是一个名为 &quot;QHSUSB_DLOAD&quot; 的未知设备
显然, 你的电脑没有正确的安装骁龙的 USB 驱动, 请去网上找一些其他可用的驱动试试&lt;/li&gt;
&lt;li&gt;电脑识别到了 &quot;Qualcomm HS-USB QDLoader 9008 (COMx)&quot; 设备, 但带有一个感叹号
你的电脑正确的安装了骁龙的 USB 驱动, 但是它似乎是旧的版本, 旧的骁龙驱动不带有驱动签名, 而在新版的 Windows 中, 驱动必须正确签名才能正常工作, 请去网上找一些其他可用的驱动试试&lt;/li&gt;
&lt;li&gt;电脑识别到了 &quot;Qualcomm HS-USB QDLoader 9008 (COMx)&quot; 设备, 但是当我松开音量键的时候, 它就会断开
尽管网上有人说这是因为安装了不合适的驱动, 但你仍然可以继续按照下面的步骤继续进行, 但是不要松开音量键了.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;如何卸载驱动?
只需要在设备管理器中, 右键设备, 单击 &quot;卸载设备&quot;, 在弹出的窗口中, 会有一个选项 &quot;卸载驱动程序&quot;, 勾选它, 然后确认即可.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;开始刷机&lt;/h2&gt;
&lt;p&gt;打开你的刷机工具, 如果是一加手机, 那它应该是 &quot;MsmDownloadTool&quot;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开你的刷机工具&lt;/li&gt;
&lt;li&gt;按照上一部分的步骤, 将手机以 9008 模式连接到电脑, 此时刷机工具中会出现你的设备, 并且状态显示 &quot;已连接&quot;&lt;/li&gt;
&lt;li&gt;点击 &quot;开始&quot;, 此时刷机工具会通过 9008 端口设备与手机通信, 传输数据&lt;/li&gt;
&lt;li&gt;等待状态一栏中出现进度条, 就可以松开音量上下键了&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;FAQ:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Param 预载失败
更换驱动&lt;/li&gt;
&lt;li&gt;自动检测 DDR 失败
手机关机, 重新进入 9008 模式, 并且重启电脑上的刷机工具&lt;/li&gt;
&lt;li&gt;进度卡住了
重刷一遍试试&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>[C#] 使用 ReadConsoleInput 所需要注意的问题.</title><link>https://slimenull.com/posts/20230627205243/</link><guid isPermaLink="true">https://slimenull.com/posts/20230627205243/</guid><description>使用 ReadConsoleInput, 需要手动启用鼠标输入, 关闭快速编辑模式, 并删去任何读取标准输入的逻辑, 只使用 ReadConsoleInput</description><pubDate>Tue, 27 Jun 2023 20:52:43 GMT</pubDate><content:encoded>&lt;p&gt;如果要使用鼠标输入, 那么需要:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;启用鼠标输入, 只有启用之后, 才能接收到鼠标事件&lt;/li&gt;
&lt;li&gt;关闭控制台的快速编辑模式, 很多人的控制台都启用了快速编辑模式, 它会导致鼠标输入无效&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;任何情况下, 都需要:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不使用除 ReadConsoleInput 之外的任何输入方法, 因为它们会和你争抢 ReadConsoleInput 的使用.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;引用:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/console/getstdhandle&quot;&gt;GetStdHandle 函数&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/console/getconsolemode&quot;&gt;GetConsoleMode 函数&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/console/setconsolemode&quot;&gt;SetConsoleMode 函数&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/console/readconsoleinput&quot;&gt;ReadConsoleInput 函数&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;internal class NativeApi
{
    public const int STD_OUTPUT_HANDLE = -11;
    public const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
    public const uint ENABLE_MOUSE_INPUT = 0x0010;
    public const int STD_INPUT_HANDLE = -10;
    public const uint ENABLE_QUICK_EDIT_MODE = 0x0040;

    [DllImport(&quot;kernel32.dll&quot;, SetLastError = true)]
    public static extern IntPtr GetStdHandle(int nStdHandle);

    [DllImport(&quot;kernel32.dll&quot;)]
    public static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);

    [DllImport(&quot;kernel32.dll&quot;)]
    public static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
    public static void EnableANSI()
    {
        var handle = GetStdHandle(STD_OUTPUT_HANDLE);
        GetConsoleMode(handle, out var mode);
        mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
        SetConsoleMode(handle, mode);
    }
    public static void EnableMouseInput()
    {
        var handle = GetStdHandle(STD_INPUT_HANDLE);
        GetConsoleMode(handle, out var mode);
        mode |= ENABLE_MOUSE_INPUT;
        mode &amp;amp;= ~ENABLE_QUICK_EDIT_MODE; //关闭快速编辑
        SetConsoleMode(handle, mode);
    }
    [DllImport(&quot;kernel32.dll&quot;, CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = &quot;ReadConsoleInputW&quot;)]
    public static extern bool ReadConsoleInput(
    IntPtr hConsoleInput,
    [Out] INPUT_RECORD[] lpBuffer,
    uint nLength,
    out uint lpNumberOfEventsRead);
    public enum InputRecordEventType : uint
    {
        KEY_EVENT = 0x1,
        MOUSE_EVENT = 0x2,
        WINDOW_BUFFER_SIZE_EVENT = 0x4,
        MENU_EVENT = 0x8,
        FOCUS_EVENT = 0x10,
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct INPUT_RECORD
    {
        [FieldOffset(0)]
        public InputRecordEventType EventType;
        [FieldOffset(4)]
        public KEY_EVENT_RECORD KeyEvent;
        [FieldOffset(4)]
        public MOUSE_EVENT_RECORD MouseEvent;
        [FieldOffset(4)]
        public WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent;
        [FieldOffset(4)]
        public MENU_EVENT_RECORD MenuEvent;
        [FieldOffset(4)]
        public FOCUS_EVENT_RECORD FocusEvent;
    };
    [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode)]
    public struct KEY_EVENT_RECORD
    {
        [FieldOffset(0), MarshalAs(UnmanagedType.Bool)]
        public bool bKeyDown;
        [FieldOffset(4), MarshalAs(UnmanagedType.U2)]
        public ushort wRepeatCount;
        // ConsoleKey
        [FieldOffset(6), MarshalAs(UnmanagedType.U2)]
        public ushort wVirtualKeyCode;
        [FieldOffset(8), MarshalAs(UnmanagedType.U2)]
        public ushort wVirtualScanCode;
        [FieldOffset(10)]
        public char UnicodeChar;
        [FieldOffset(12), MarshalAs(UnmanagedType.U4)]
        public ControlKeyState dwControlKeyState;
    }

    // dwControlKeyState bitmask
    [Flags]
    public enum ControlKeyState
    {
        RIGHT_ALT_PRESSED = 0x1,
        LEFT_ALT_PRESSED = 0x2,
        RIGHT_CTRL_PRESSED = 0x4,
        LEFT_CTRL_PRESSED = 0x8,
        SHIFT_PRESSED = 0x10,
        NUMLOCK_ON = 0x20,
        SCROLLLOCK_ON = 0x40,
        CAPSLOCK_ON = 0x80,
        ENHANCED_KEY = 0x100
    }
    [StructLayout(LayoutKind.Sequential)]
    public struct MOUSE_EVENT_RECORD
    {
        public COORD dwMousePosition;
        public MouseButtonState dwButtonState;
        public short MouseDelta;
        public ControlKeyState dwControlKeyState;
        public MouseEventFlags dwEventFlags;
    }

    [Flags]
    public enum MouseButtonState : short
    {
        FROM_LEFT_1ST_BUTTON_PRESSED = 0x1,
        RIGHTMOST_BUTTON_PRESSED = 0x2,
        FROM_LEFT_2ND_BUTTON_PRESSED = 0x4,
        FROM_LEFT_3RD_BUTTON_PRESSED = 0x8,
        FROM_LEFT_4TH_BUTTON_PRESSED = 0x10
    }
    [StructLayout(LayoutKind.Sequential)]
    public struct COORD
    {
        public short X;
        public short Y;

        public COORD(short X, short Y)
        {
            this.X = X;
            this.Y = Y;
        }
    };
    [Flags]
    public enum MouseEventFlags
    {
        MOUSE_MOVED = 0x1,
        DOUBLE_CLICK = 0x2,
        MOUSE_WHEELED = 0x4,
        MOUSE_HWHEELED = 0x8
    }
    public struct WINDOW_BUFFER_SIZE_RECORD
    {
        public COORD dwSize;

        public WINDOW_BUFFER_SIZE_RECORD(short x, short y)
        {
            dwSize = new COORD(x, y);
        }
    }
    [StructLayout(LayoutKind.Sequential)]
    public struct MENU_EVENT_RECORD
    {
        public uint dwCommandId;
    }
    [StructLayout(LayoutKind.Sequential)]
    public struct FOCUS_EVENT_RECORD
    {
        public uint bSetFocus;
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[.NET/C#] 委托与 Lambda 表达式, 语句详解</title><link>https://slimenull.com/posts/20230529214634/</link><guid isPermaLink="true">https://slimenull.com/posts/20230529214634/</guid><description>在 C# 中, 委托与 Lambda 是最常用的功能之一, 诸如多线程, 可迭代类型的 LINQ 操作, 回调, 都是借助委托完成的. 它在 C 语言对应的是函数指针, 在 C++ 中对应的是 `std::function`</description><pubDate>Mon, 29 May 2023 21:46:34 GMT</pubDate><content:encoded>&lt;p&gt;在 C# 中, 委托与 Lambda 是最常用的功能之一, 诸如多线程, 可迭代类型的 LINQ 操作, 回调, 都是借助委托完成的. 它在 C 语言对应的是函数指针, 在 C++ 中对应的是 &lt;code&gt;std::function&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;委托 / Delegate&lt;/h2&gt;
&lt;p&gt;在 C# 中, 委托是一种特殊的类型, 它用来存储一段逻辑(可执行的内容). 例如在声明字段或变量的时候, 我们指定类型为委托, 那么这个字段或变量就可以存下一段逻辑. 在你需要的时候, 就可以直接执行它.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void MethodA()
{
    // 这是一个方法, 里面包含一些逻辑
    // 这个方法没有参数, 也没有返回值
    Console.WriteLine(&quot;Hello world&quot;);
}

void MethodB()
{
    // 声明一个能存储下一段逻辑的变量
    // Action 表示无参数无返回值的逻辑
    Action someLogic;

    // 为其赋值, 可以直接将方法赋值过去
    someLogic = MethodA;

    // 调用这段逻辑
    // 因为它存储的就是 MethodA, 所以等同于调用 MethodA
    someLogic.Invoke();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事实上, 在使用委托之前, 我们需要定义一个委托类型, 在上述代码中使用的 Action, 就是系统预定义好的一个委托类型. 如果我们需要为带有特定参数, 特定返回值的一段逻辑定义一个类型, 可以使用 &lt;code&gt;delegate&lt;/code&gt; 关键字.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义一个无参数, 无返回值委托类型
// 其中, MyDelegate 是委托类型名, public 是访问修饰
public delegate void MyDelegate();

// 定义一个带有 int 参数, 返回值为 string 的委托类型
public delegate string MyDelegate2(int);

// 定义一个带有 int, float 参数, 返回值为 string 的委托类型
public delegate string MyDelegate3(int, float);

// 如果你想, 你也可以为委托类型定义中的参数写上名字
// 这个名字也只是为了可读性, 让用户知道这个参数是做什么用的
// 除此之外, 就么有什么别的用处了
public delegate string MyDelegate4(int value1, float value2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要使用定义好的委托类型, 和最开始给出的示例代码一样, 只需要把他当成一个普通类型直接使用即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义一个 someLogic 变量, 类型为 MyDelegate
// 同时, 为其赋初始值 MethodA
MyDelegate someLogic = MethodA;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可以通过委托类型来存下任何你想要存下的逻辑, 只要类型和对应逻辑的参数返回值是匹配的即可. 下面是定义一个委托类型, 然后将 &lt;code&gt;Console.WriteLine&lt;/code&gt; 这个方法存入变量的完整示例代码.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace TestConsole
{
    class Program
    {
        // 定义参数为 string, 无返回值的委托类型
        delegate void MyDelegate(string text);
        
        static void Main(string[] args)
        {
            // 定义变量
            MyDelegate someLogic;
            
            // 为其赋值
            // 因为 Console.WriteLine 有匹配的重载, 所以可以存入
            someLogic = Console.WriteLine;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不仅是诸如 &lt;code&gt;Console.WriteLine&lt;/code&gt; 的静态方法可以存入, 非静态的方法也是可以存入的. 下面是一段将实例方法存入变量的完整示例代码.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace TestConsole
{
    class TestClass
    {
        public void TestMethod()
        {
            var x = 1;
            var y = 2;
            var z = x + y;
            System.Console.WriteLine(z);
        }
    }

    delegate void TestDelegate();

    class Program
    {
        static void Main(string[] args)
        {
            // 创建类型的实例
            TestClass ins = new TestClass();

            // 定义委托变量
            TestDelegate someLogic;

            // 将实例的方法存入到委托变量
            someLogic = ins.TestMethod;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;匿名函数 / Lambda&lt;/h2&gt;
&lt;p&gt;在上面的例子中, 我们将方法存入了委托变量中, 但如果每一段逻辑都需要定义一个方法, 那就太麻烦了. 使用 Lambda 表达式就是一个很不错的选择.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Action someLogic;

someLogic = () =&amp;gt;
{
    Console.WriteLine(&quot;Hello world&quot;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的代码中, 我们定义了一个无参无返回值的委托变量, 并使用了一个奇怪的语法为其赋值. 这个语法, 就是 Lambda 表达式. 其中括号表示参数, &lt;code&gt;=&amp;gt;&lt;/code&gt; 是 lambda 的符号, 后面的大括号是逻辑的内容. 因为无参数, 所以括号内是空的.&lt;/p&gt;
&lt;p&gt;Lambda 语句的标准语法如下:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(输入参数) =&amp;gt; { &amp;lt;多条语句&amp;gt; }&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面我们以一个执行加法计算的 Lambda 语句为例, 了解 Lambda 语句的具体写法.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义一个传入两个 int 参数, 返回一个 int 结果的委托变量
Func&amp;lt;int, int, int&amp;gt; someLogic;

// 为委托变量赋值
someLogic = (int a, int b) =&amp;gt;
{
    return a + b;
};

// 执行委托, 并打印返回值
// 输出内容是 114514
Console.WriteLine(someLogic.Invoke(114000, 514));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lambda 语句可以理解为一个没有名字的方法, 在上面的代码中, 我们所写的 Lambda 表达式传入两个 &lt;code&gt;int&lt;/code&gt;, 并使用 &lt;code&gt;return&lt;/code&gt; 返回他们的相加结果. 这个 Lambda 语句直接被赋值到 someLogic 变量中, 并在后续的代码中, 通过 someLogic 调用刚刚缩写的逻辑.&lt;/p&gt;
&lt;p&gt;事实上, 因为编译器知道 someLogic 的委托类型需要传入两个 &lt;code&gt;int&lt;/code&gt;, 所以在后面直接赋值的 Lambda 语句中, 参数的类型是可以省略的. 编译器会自动推导他们的类型.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 省略参数的类型, 直接写参数名
someLogic = (a, b) =&amp;gt;
{
    return a + b;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 Lambda 语句的传入参数只有一个, 那么括号也是可以被省略的.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义一个传入参数为 int, 无返回值的委托变量
Action&amp;lt;int&amp;gt; someLogic;

// 为其赋值
someLogic = integer =&amp;gt;
{
    Console.WriteLine(integer);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你不需要使用传入的参数, 可以使用 &lt;code&gt;_&lt;/code&gt; 将其丢弃:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义一个传入参数为 int, 无返回值的委托变量
Action&amp;lt;int&amp;gt; someLogic;

// 为其赋值
// 这里的表达式没有使用传入的参数
someLogic = _ =&amp;gt; Console.WriteLine(&quot;Hello world&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 Lambda 语句的方法体只有一个语句, 那么大括号是可以被省略的. 此时 Lambda 语句就叫做 Lambda 表达式了.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义一个传入参数为 int, 无返回值的委托变量
Action&amp;lt;int&amp;gt; someLogic;

// 为其赋值
someLogic = integer =&amp;gt; Console.WriteLine(integer);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你要在 Lambda 中使用 await, 可以在 Lambda 前添加 async 关键字, 使用方式和普通的方法是一样的.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义一个无传入参数, 返回值为 Task&amp;lt;int&amp;gt; 的委托变量
Func&amp;lt;Task&amp;lt;int&amp;gt;&amp;gt; someLogic;

// 为其赋值, 并使用 async Lambda
someLogic = async () =&amp;gt;
{
    // 延时 1s
    await Task.Delay(1000);

    // 返回值
    return 114514;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果只是单纯调用一个委托, 那么也可以不需要使用 &lt;code&gt;Invoke&lt;/code&gt;, 直接把他当成方法, 后跟括号和参数即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Action&amp;lt;string&amp;gt; someLogic = str =&amp;gt; Console.WriteLine(str);

someLogic.Invoke(&quot;Hello world&quot;);

// 等同于上面的 Invoke
someLogic(&quot;Hello world&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;捕获 / Capture&lt;/h2&gt;
&lt;p&gt;在使用 Lambda 语句/表达式的时候, Lambda 可以捕获到外部的变量, 字段, 或属性, 简单来讲就是, 你可以直接在 Lambda 内使用 Lambda 外的任何东西.&lt;/p&gt;
&lt;p&gt;读取 Lambda 外的值, 并且 Lambda 能感知到外部对值的变更.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int num = 0;
Action action = () =&amp;gt; Console.WriteLine(num);

// 输出 0
action.Invoke();

num += 123;

// 输出 123 (外部对变量变更时, lambda 仍然能输出正确的值)
action.Invoke();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对 Lamda 外部的值进行更改.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int num = 0;
Action action = () =&amp;gt; num++;

for (int i = 0; i &amp;lt; 3; i++)
    action.Invoke();

// 输出 3
Console.WriteLine(num);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用 / Usage&lt;/h2&gt;
&lt;p&gt;下面列举几种使用 Lambda 语句/表达式的例子.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 一句话启动新线程
new Thread(() =&amp;gt;
{
    int i = 0;
    while (true)
    {
        Console.WriteLine(i++);
        Thread.Sleep(1000);
    }
}).Start();

// 一句话启动新任务
Task.Run(async () =&amp;gt;
{
    int i = 0;
    while (true)
    {
        Console.WriteLine(i++);
        await Task.Delay(1000);
    }
});

// 生成 10 个随机数
List&amp;lt;int&amp;gt; numbers = Enumerable.Range(0, 10)
    .Select(_ =&amp;gt; Random.Shared.Next())
    .ToList();

// 筛选上面随机数中大于 100 的
List&amp;lt;int&amp;gt; filteredNumbers = numbers
    .Where(num =&amp;gt; num &amp;gt; 100)
    .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;编译 / Compilation&lt;/h2&gt;
&lt;p&gt;Lambda 本质上是一个语法糖, 在编译时, Lambda 会被编译成方法, 或者一个类, 然后其中包含的一个方法.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以下实例, 为了可读性, 均在编译器编译后结果的基础上进行了命名上的优化, 以方便理解.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;https://sharplab.io/#v2:EYLgtghglgdgNAExAagD4AEBMBGAsAKHQGYACLM7AdhIG8CSGKA2MgFhIFloYAKdbAAwBtALokIAJwDmAZwCUteo2WwALiRgBXMCQC8JAQG4lyhv0ziAxqqgB7GHpI8FugHwUAnDy1g5x/CamAPRBJIDJ8YBfigaByhDWdjAAdACSMAButgDWAKbO/jGMPiTI+tiYRPn4powh4VFlpDyAaJqAFwmAn9qAG8qA84kdgC9mgG+mcCQANhBgwAgQJICzcoBsjoC/AZGAxtaAdh6AIW6APApyBQxxNvYp6Vm5foEAvgRnQA=&quot;&gt;示例一:捕获局部变量&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System;
public class Program {
    static void Main(string[] args) {
        int num = 0;
        Action action = () =&amp;gt; Console.WriteLine(num);

        // 输出 0
        action.Invoke();

        num += 123;

        // 输出 123 (外部对变量变更时, lambda 仍然能输出正确的值)
        action.Invoke();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译后源码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Program
{
    [CompilerGenerated]
    private sealed class LambdaClass
    {
        public int num;

        internal void LambdaMethod()
        {
            Console.WriteLine(num);
        }
    }

    private static void Main(string[] args)
    {
        LambdaClass lambdaInstance = new LambdaClass();
        lambdaInstance.num = 0;
        Action action = new Action(lambdaInstance.LambdaMethod);
        action();
        lambdaInstance.num += 123;
        action();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到, 在上面的示例中, Lambda 表达式被编译成了一个类, 并且捕获到的局部变量均变成了类的字段. 而不再是存储在栈上.&lt;/p&gt;
&lt;p&gt;由此, 就诞生了一个问题, 当你尝试对一个被捕获的局部变量取地址的时候, 就会报错. 我们没办法取一个位于堆上的对象中. 堆上的对象会被 GC 移动. 或者说, 如果一个局部变量被取地址, 就意味着它需要在栈上, 而存储在栈上的值, 没办法被 Lambda 所捕获.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;https://sharplab.io/#v2:EYLgtghglgdgNAExAagD4AEBMBGAsAKHQGYACLM7AdhIG8CSGTYAXEmAVzBIF4SAGANz1GwhugAsJAMoB7MAFMAsvOYALGQgAU6bHwDaAXRIQATgHMAzgEpaoxmOyZjAY2ZQZMHiU03uAPgoATk0OMCshfDt7AHpokkBk+MAvxX4oxghXdxgAOgBJGAA3GQBreR8I1IZQkmReRyJy/HsYuKSSOu9ANE1AC4TAT+1ADeVAecS+wBezQDfTOBIAGwgwYAQIEkBZuUA2R0BfgKTAY2tAOw9AELdAHgUrCpc3D1yC4tLwuwBfAmugA==&quot;&gt;示例二:捕获实例字段&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System;
public class Program {
    int num = 0;
    
    void SomeMethod(string[] args) {
        Action action = () =&amp;gt; Console.WriteLine(num);

        // 输出 0
        action.Invoke();

        num += 123;

        // 输出 123 (外部对变量变更时, lambda 仍然能输出正确的值)
        action.Invoke();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译后源码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Program
{
    private int num = 0;

    private void SomeMethod(string[] args)
    {
        Action action = new Action(LambdaMethod);
        action();
        num += 123;
        action();
    }

    [CompilerGenerated]
    private void LambdaMethod()
    {
        Console.WriteLine(num);
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[.NET/WPF] 如何使用 CommunityToolkit.Mvvm</title><link>https://slimenull.com/posts/20230512125059/</link><guid isPermaLink="true">https://slimenull.com/posts/20230512125059/</guid><description>CommunityToolkit.Mvvm 基础使用</description><pubDate>Fri, 12 May 2023 12:50:59 GMT</pubDate><content:encoded>&lt;p&gt;CommunityToolkit.Mvvm 包（又名 MVVM 工具包，以前称为 Microsoft.Toolkit.Mvvm）是一个现代、快速和模块化的 MVVM 库。 它是 .NET Community Toolkit 的一部分，并围绕以下原则构建：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;独立于平台和运行时 - .NET Standard 2.0、.NET Standard 2.1 和 .NET 6 （与 UI 框架无关）&lt;/li&gt;
&lt;li&gt;易于上手和使用 — 对应用程序结构或编码范例（“MVVM”之外）没有严格要求，即灵活使用。&lt;/li&gt;
&lt;li&gt;可选 - 自由选择要使用的组件。&lt;/li&gt;
&lt;li&gt;引用实现 - 精益和高性能，为基类库中包含的接口提供实现，但缺乏直接使用它们的具体类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;可观察对象 / ObservableObject&lt;/h2&gt;
&lt;p&gt;在 WPF 中， 写出 MVVM 设计模式的程序， 自然需要进行前台与后台数据的绑定， 而在原生实现中， 编写一个可绑定的类， 并且编写该类中可绑定的成员， 是非常麻烦的一件事情。 你需要手动在值变更的时候， 引发 “PropertyChanged” 事件， 大概的代码会像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MainViewModel : INotifyPropertyChanged
{
    private string windowTitle = &quot;Toturial App&quot;;
    public string WindowTitle
    {
        get =&amp;gt; windowTitle;
        set
        {
            windowTitle = value;
            NotifyPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged; 

    protected void NotifyPropertyChanged([CallerMemberName] string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;像这样， 每一个属性都需要编写一个对应的字段， 然后手动编写 getter， setter， 并在 setter 中进行属性变更的通知，显然， 非常麻烦。&lt;/p&gt;
&lt;p&gt;在 CommunityToolkit.Mvvm 中， 提供了用于生成这些重复性代码的源生成器， 我们只需要简单的在需要的地方加上一些特性， 就可以实现这些复杂的东西了。 例如， 上述代码在 CommunityToolkit.Mvvm 中， 代码是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ObservableObject]
public partial class MainViewModel
{
    [ObservableProperty]
    private string windowTitle = &quot;Toturial App&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为所需的 ViewModel 添加 ObservableObject 特性， 利用分布类， 他会自动生成该类型对 INotifyPropertyChanged 的实现。 而打上了 ObservableProperty 特性的字段， 源生成器会自动生成该字段名称转换为大驼峰之后的一个属性， 并在属性定义中包含对属性变更的通知逻辑。&lt;/p&gt;
&lt;p&gt;简单来讲， 我们不需要写那么复杂的逻辑了。 只需要为可观察的类型打上 ObservableObject， 为可观察的属性打上 ObservableProperty， 其余的， 都会自动生成。&lt;/p&gt;
&lt;h2&gt;RelayCommand / 中继指令&lt;/h2&gt;
&lt;p&gt;同样的， 在原生 WPF 中， 想要定义一个指令， 也是非常麻烦的一件事。 你需要为指令专门写一个类， 并实现 ICommand 接口。 在这个接口中分别包含了判断指令是否能够执行，执行指令的方法，以及一个用于通知 “指令可执行性变更” 的事件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyCommand : ICommand
{
    public bool CanExecute(object parameter)
    {
        // 这里写判断命令是否可执行的逻辑
    }

    public void Execute(object parameter)
    {
        // 这里是命令执行的逻辑
    }

    // 这里是用于通知指令可执行性变更的事件
    public event EventHandler? CanExecuteChanged;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是很多时候， 我们的指令只是一些简单的逻辑， 为一个简单逻辑编写一个类， 未免有些太麻烦了。 于是 CommunityToolkit.Mvvm 中为我们提供了一个特性：RelayCommand。&lt;/p&gt;
&lt;p&gt;当为一个方法打上 RelayCommand 标记的时候， CommunityToolkit.Mvvm 会在该方法所在的类下生成一个对应的 Command， 你可以在界面上直接绑定到这个 Command， 而不需要自己手动编写一个 Command 类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public partial class MainWindow
{
    public MainWindow()
    {

        // 将绑定上下文设为自身
        DataContext = this;
        InitalizeComponents();
    }

    // 这里会生成一个 ShowMessageCommand 属性
    [RelayCommand]
    public void ShowMessage()
    {
        MessageBox.Show(&quot;Hello world!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Window ...&amp;gt;
    &amp;lt;Grid&amp;gt;
        &amp;lt;StackPanel&amp;gt;
            &amp;lt;!--直接绑定到后台的 ShowMessageCommand--&amp;gt;
            &amp;lt;Button Content=&quot;Click Me&quot; Command=&quot;{Binding ShowMessageCommand}&quot;/&amp;gt;
        &amp;lt;/StackPanel&amp;gt;
    &amp;lt;/Grid&amp;gt;
&amp;lt;/Window&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[八方旅人] 魔大公攻略, 可行的一种打法</title><link>https://slimenull.com/posts/20230504134503/</link><guid isPermaLink="true">https://slimenull.com/posts/20230504134503/</guid><description>八方旅人, 魔大公攻略</description><pubDate>Thu, 04 May 2023 13:45:03 GMT</pubDate><content:encoded>&lt;p&gt;我认为魔大公最难的一点就是, 会释放超绝法术. 一旦释放, 就算有 700 的魔抗, 3000 的血量, 也要团灭. 所以最关键的一点就是, 不让魔大公有释放法术的机会.&lt;/p&gt;
&lt;h2&gt;要求&lt;/h2&gt;
&lt;p&gt;等级大概是 40 级, 但是装备不能太拉跨, 血量大概两千, 得保证不能被魔大公秒掉, 并且神官能在一次恢复魔法后将队伍血量回满.&lt;/p&gt;
&lt;p&gt;需要一些光和暗的精灵石, 任意等级皆可, 主要用于破防.&lt;/p&gt;
&lt;h2&gt;阵容搭配&lt;/h2&gt;
&lt;p&gt;我这边的打法, 需要两个学者, 一个猎人, 一个盗贼以及一个神官. 两个学者均需要大火冰雷法术(用来破防以及打出主要伤害), 猎人需要 &apos;五月雨箭&apos;(用于破防), 盗贼需要 &apos;吸血匕首&apos; 或 &apos;吸魔匕首&apos;(用于破防).&lt;/p&gt;
&lt;p&gt;想多拿点钱, 可以让商人使用奥义 (打多少血, 给多少钱), 伤害低的话, 大概每次 5000$ 的样子, 如果伤害够, 一次 7000 是没问题的. 整个战斗中大概能用个 5 次(不是不能用更多次, 而是打这么多次, 魔大公已经要死掉了). 另外, 对魔大公进行收钱, 是能收到 40000 的.&lt;/p&gt;
&lt;p&gt;神官需要有大恢复魔法, 如果没有, 那得保证增强的恢复魔法能够将队伍血量补满, 保证每一次破防后, 队伍状态能回满.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;角色&lt;/th&gt;
&lt;th&gt;主职业&lt;/th&gt;
&lt;th&gt;副职业&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;特蕾莎&lt;/td&gt;
&lt;td&gt;商人&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;学者&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;塞拉斯&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;学者&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;盗贼&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;泰里翁&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;盗贼&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;猎人&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;欧菲莉亚&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;神官&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;舞女&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;BOSS 弱点&lt;/h2&gt;
&lt;p&gt;BOSS 在初始状态破防之后, 会在下面的三个阶段中不断切换, 按照阵容中的搭配, 可以轻松对其破防.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;状态&lt;/th&gt;
&lt;th&gt;弱点&lt;/th&gt;
&lt;th&gt;盾&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;初始状态&lt;/td&gt;
&lt;td&gt;剑, 枪, 斧, 雷, 光&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;阶段 1&lt;/td&gt;
&lt;td&gt;剑, 雷, 风&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;阶段 2&lt;/td&gt;
&lt;td&gt;弓, 光, 暗&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;阶段 3&lt;/td&gt;
&lt;td&gt;短剑, 火, 冰&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;要点&lt;/h2&gt;
&lt;p&gt;猎人需要在整个战斗中, 始终保证对方有 &quot;黏着丝网&quot; 的状态, 这样能够保证: 在当前魔大公防御恢复的回合, 魔大公攻击顺序处于最开始, 而在第二个回合, 由于黏着丝网, 魔大公攻击顺序处于最后. 这样, 队伍的攻击顺序就集中在中间, 总共八次攻击, 对其破防也是轻轻松松.&lt;/p&gt;
&lt;p&gt;&amp;lt;table&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td colspan=6&amp;gt;第一回合&amp;lt;/td&amp;gt;
&amp;lt;td colspan=6&amp;gt;第二回合&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td&amp;gt;&amp;lt;strong&amp;gt;魔大公&amp;lt;/strong&amp;gt;&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;&amp;lt;strong&amp;gt;魔大公&amp;lt;/strong&amp;gt;&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;我方&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;我方&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;我方&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;我方&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;我方&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;我方&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;我方&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;我方&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;&amp;lt;strong&amp;gt;魔大公&amp;lt;/strong&amp;gt;&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;&amp;lt;strong&amp;gt;魔大公&amp;lt;/strong&amp;gt;&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;&lt;/p&gt;
&lt;p&gt;这样, 魔大公就有了这样的行为: 每一次防御恢复, 都会被后面队伍八次攻击打破防, 然后队伍进行输出, 而下一次防御恢复, 魔大公又会被直接打晕, 如此往复.&lt;/p&gt;
&lt;p&gt;魔大公每一次防御恢复的时候, 都会有以下两次行为: 调整姿势(也就是切换阶段, 弱点会变更), 进行全体法术攻击(只要能抗住, 这场战斗就已经宣告你的胜利了)&lt;/p&gt;
&lt;h2&gt;流程&lt;/h2&gt;
&lt;p&gt;进入战斗的时候, 假定魔大公先手攻击, 它会直接给你一个威慑的全体 debuff, 然后进行一次法术攻击(需要硬抗). 抗住之后直接进行破盾, 两个学者释放大雷法术, 直接破防. 到猎人的回合, 不要犹豫, 直接给 &quot;黏着丝线&quot; 的 debuff.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一旦魔大公没有黏着丝线的 debuff, 它就有机会给自己一个魔法超绝化 buff, 这时它的魔法攻击就能直接将我方团灭. 即便是 700 魔抗, 3000 血量也无济于事. 所以&lt;strong&gt;整场战斗&lt;/strong&gt;, &lt;strong&gt;必须&lt;/strong&gt; 要 &lt;strong&gt;保证敌方始终有黏着丝线的 debuff&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在初始阶段将它打破防之后, 不要着急输出, 将它初始阶段的所有弱点全部使用学者的技能探明, 不要心疼 BP, 直接开 BP 探明.&lt;/p&gt;
&lt;p&gt;轮到神官回合, 直接将队伍血量回满. 神官应该是有两个回合, 因为魔大公没办法秒杀我们, 并且每一次恢复都会被我方打破防, 所以已经能确认战斗胜利了. 接下来需要的就是耗它. 你也可以用神官给自己套个反射轻纱, 恶心一下对方(伤害不强, 污辱性极强).&lt;/p&gt;
&lt;p&gt;在进行以上操作之后, 会有以下的效果: 魔大公处于破防状态, 每一次防御恢复, 都会再次被打晕.&lt;/p&gt;
&lt;h2&gt;对策&lt;/h2&gt;
&lt;p&gt;魔大公在不同的阶段下, 可以使用以下对策进行破防:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;状态&lt;/th&gt;
&lt;th&gt;弱点&lt;/th&gt;
&lt;th&gt;盾&lt;/th&gt;
&lt;th&gt;对策&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;初始状态&lt;/td&gt;
&lt;td&gt;剑, 枪, 斧, 雷, 光&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;学者直接放大魔法即可&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;阶段 1&lt;/td&gt;
&lt;td&gt;剑, 雷, 风&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;学者放大魔法, 共四次攻击, 8 次伤害, 直接破防&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;阶段 2&lt;/td&gt;
&lt;td&gt;弓, 光, 暗&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;猎人两次 &apos;五月箭雨&apos;, 至少破掉 6 盾, 神官两次光明魔法破调 2 盾, 如果有剩下的, 使用精灵石或者商人两次普通攻击破掉&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;阶段 3&lt;/td&gt;
&lt;td&gt;短剑, 火, 冰&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;学者放大魔法, 共四次攻击, 8 次伤害, 剩余两盾直接盗贼吸血匕首或者吸魔匕首直接打掉&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;破防后, 优先进行 HP 恢复, 如果敌方的 &apos;黏着捕网&apos; debuff 回合数不足, 就赶紧补上.&lt;/p&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;p&gt;千万一定要在魔大公防御恢复后直接将其破防, 不给他出手的机会, 否则就会团灭. 时不时注意自己的蓝条, 如果要用大魔法的时候, 没有蓝了, 那会很吓人的.&lt;/p&gt;
</content:encoded></item><item><title>[Win32] 窗体暗色模式, C++, WinForm, WPF 使用方法, 判断颜色模式, 响应颜色变更消息, 设置标题栏暗色.</title><link>https://slimenull.com/posts/20230404173208/</link><guid isPermaLink="true">https://slimenull.com/posts/20230404173208/</guid><description>Win32 暗色模式适配, C++, WinForm, WPF 判断当前颜色模式, 响应颜色变更消息, 设置标题栏暗色</description><pubDate>Tue, 04 Apr 2023 17:32:08 GMT</pubDate><content:encoded>&lt;p&gt;2023 年了, 如果咱写的程序还不支持暗色模式, 那就说不过去了.&lt;/p&gt;
&lt;h2&gt;判断是否是暗色模式&lt;/h2&gt;
&lt;p&gt;在 Windows 中判断当前系统的颜色模式是否是暗色, 可以通过查询注册表项来实现.&lt;/p&gt;
&lt;p&gt;下面是 C++ 的示例代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HKEY hKey;
const wchar_t* subKey = L&quot;SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize&quot;;
const wchar_t* valueName = L&quot;AppsUseLightTheme&quot;;
DWORD value = -1;
DWORD valueSize = sizeof(DWORD);
if (RegOpenKeyEx(HKEY_CURRENT_USER, subKey, 0, KEY_READ, &amp;amp;hKey) == ERROR_SUCCESS)
{
    HRESULT hr = RegGetValue(hKey, nullptr, valueName, RRF_RT_REG_DWORD, nullptr, &amp;amp;value, &amp;amp;valueSize);
    if (hr != S_OK)
    {
        value = -1;    // 不要假定该键必须存在，如果找不到将返回默认值
    }
    RegCloseKey(hKey);
}

if (value == 0)
{
    // 当前使用暗色主题
}
else if (value == 1)
{
    // 当前使用亮色主题
}
else
{
    // 无法确定当前主题
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是 C# 示例代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Microsoft.Win32;

RegistryKey key = Registry.CurrentUser.OpenSubKey(@&quot;SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize&quot;);
if (key != null)
{
    int appsUseLightTheme = (int)key.GetValue(&quot;AppsUseLightTheme&quot;, -1);
    if (appsUseLightTheme == 0)
    {
        // 当前使用暗色主题
    }
    else if (appsUseLightTheme == 1)
    {
        // 当前使用亮色主题
    }
    else
    {
        // 无法确定当前主题
    }
    key.Close();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;设置窗口暗色模式&lt;/h2&gt;
&lt;p&gt;Windows 中自带了一个暗色模式的 WinAPI, 直接调用就可以将窗口标题栏变成暗色.&lt;/p&gt;
&lt;p&gt;下面是 C++ 的示例代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;windows.h&amp;gt;
#include &amp;lt;dwmapi.h&amp;gt;

int main() {
    HWND hWnd = GetMainWindowHandle(); // 获取主窗口句柄
    DWORD dwAttribute = DWMWA_USE_IMMERSIVE_DARK_MODE; // 设置暗色模式属性
    BOOL bValue = TRUE; // 启用暗色模式
    DwmSetWindowAttribute(hWnd, dwAttribute, &amp;amp;bValue, sizeof(bValue)); // 设置窗口属性

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是 C# 的示例代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[DllImport(&quot;dwmapi.dll&quot;, PreserveSig = true)]
public static extern int DwmSetWindowAttribute(IntPtr hwnd, DwmWindowAttribute attr, ref int attrValue, int attrSize);

public static bool EnableDarkModeForWindow(IntPtr hWnd, bool enable)
{
    int darkMode = enable ? 1 : 0;
    int hr = DwmSetWindowAttribute(hWnd, DwmWindowAttribute.UseImmersiveDarkMode, ref darkMode, sizeof(int));
    return hr &amp;gt;= 0;
}

public enum DwmWindowAttribute : uint
{
    NCRenderingEnabled = 1,
    NCRenderingPolicy,
    TransitionsForceDisabled,
    AllowNCPaint,
    CaptionButtonBounds,
    NonClientRtlLayout,
    ForceIconicRepresentation,
    Flip3DPolicy,
    ExtendedFrameBounds,
    HasIconicBitmap,
    DisallowPeek,
    ExcludedFromPeek,
    Cloak,
    Cloaked,
    FreezeRepresentation,
    PassiveUpdateMode,
    UseHostBackdropBrush,
    UseImmersiveDarkMode = 20,
    WindowCornerPreference = 33,
    BorderColor,
    CaptionColor,
    TextColor,
    VisibleFrameBorderThickness,
    SystemBackdropType,
    Last
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;响应配色更改&lt;/h2&gt;
&lt;p&gt;当系统配色更改的时候, 会向所有窗体发送一个 &lt;code&gt;THEMECHANGED&lt;/code&gt; 消息:&lt;/p&gt;
&lt;p&gt;C++ 中, 你可以在 WndProc 捕捉这个消息, 然后进行具体操作. 例如上面提到的, 设置窗口暗色模式.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;Windows.h&amp;gt;
#include &amp;lt;iostream&amp;gt;

LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int main() {
    // 注册窗口类
    WNDCLASS wc = {};
    wc.lpfnWndProc = WndProc;
    wc.hInstance = GetModuleHandle(NULL);
    wc.lpszClassName = L&quot;MyWindowClass&quot;;
    if (!RegisterClass(&amp;amp;wc)) {
        std::cerr &amp;lt;&amp;lt; &quot;Failed to register window class&quot; &amp;lt;&amp;lt; std::endl;
        return 1;
    }

    // 创建窗口
    HWND hwnd = CreateWindow(
        L&quot;MyWindowClass&quot;,
        L&quot;My Window&quot;,
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
        NULL, NULL, GetModuleHandle(NULL), NULL
    );
    if (!hwnd) {
        std::cerr &amp;lt;&amp;lt; &quot;Failed to create window&quot; &amp;lt;&amp;lt; std::endl;
        return 1;
    }

    // 显示窗口
    ShowWindow(hwnd, SW_SHOWDEFAULT);

    // 消息循环
    MSG msg;
    while (GetMessage(&amp;amp;msg, NULL, 0, 0)) {
        TranslateMessage(&amp;amp;msg);
        DispatchMessage(&amp;amp;msg);
    }

    return 0;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    case WM_THEMECHANGED:
        std::cout &amp;lt;&amp;lt; &quot;Theme changed&quot; &amp;lt;&amp;lt; std::endl;
        return 0;
    default:
        return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C# 的话, 如果你在使用 WinForm 或 WPF, 你可以直接使用 C# 封装好的 &lt;code&gt;SystemEvents&lt;/code&gt; 类, 它有一个 &lt;code&gt;UserPreferenceChanged&lt;/code&gt; 事件, 通过订阅这个事件, 并判断具体分类, 最后执行操作:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged;

private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
{
    if (e.Category == UserPreferenceCategory.General)
    {
        // 更新颜色主题
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[WPF] 资源, 静态资源, 动态资源的使用及注意事项. 使用资源字典进行 “语言国际化“ 与 “颜色模式切换“</title><link>https://slimenull.com/posts/20230403133618/</link><guid isPermaLink="true">https://slimenull.com/posts/20230403133618/</guid><description>资源, 静态与动态资源, 基于资源实现语言国际化及配色切换</description><pubDate>Mon, 03 Apr 2023 13:36:18 GMT</pubDate><content:encoded>&lt;p&gt;资源是 WPF 里面的一个好东西, 你可以在里面存很多东西, 然后直接在 XAML 或 CS 代码中调用获取对应的值. 任何东西都可以存进去, 只要它是可以由文本表示的, 那就可以.&lt;/p&gt;
&lt;h2&gt;定义资源的方式&lt;/h2&gt;
&lt;p&gt;资源定义有很多方式, 它们也有不同的效果, 作用域, 以及合适的用途.&lt;/p&gt;
&lt;h3&gt;1. 在控件中定义资源&lt;/h3&gt;
&lt;p&gt;在 WPF 中, 任何一个 &lt;code&gt;FrameworkElement&lt;/code&gt; (框架元素) 都可以用来定义资源, 它有一个 &lt;code&gt;Resources&lt;/code&gt; 属性, 我们使用它来定义所需资源即可.&lt;/p&gt;
&lt;p&gt;在一个控件中定义的资源, 这个资源也只能在这个控件中被访问, 外部是无法访问到这个资源的.&lt;/p&gt;
&lt;p&gt;下面是一个在 &lt;code&gt;StackPanel&lt;/code&gt; 中定义一个适用于 &lt;code&gt;Button&lt;/code&gt; 的 &lt;code&gt;Style&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;StackPanel&amp;gt;
  &amp;lt;StackPanel.Resources&amp;gt;
    &amp;lt;Style TargetType=&quot;Button&quot;&amp;gt;
      &amp;lt;Setter Property=&quot;Margin&quot; Value=&quot;3&quot;/&amp;gt;
    &amp;lt;/Style&amp;gt;
  &amp;lt;/StackPanel.Resources&amp;gt;
  
  &amp;lt;Button&amp;gt;Click&amp;lt;/Button&amp;gt;
  &amp;lt;Button&amp;gt;Me&amp;lt;/Button&amp;gt;
  &amp;lt;Button&amp;gt;To&amp;lt;/Button&amp;gt;
  &amp;lt;Button&amp;gt;Go&amp;lt;/Button&amp;gt;
&amp;lt;/StackPanel&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的代码中, &lt;code&gt;StackPanel&lt;/code&gt; 控件的 &lt;code&gt;Resources&lt;/code&gt; 中定义了一个 &lt;code&gt;Style&lt;/code&gt;, 这个 &lt;code&gt;Style&lt;/code&gt; 的目标类型是 &lt;code&gt;Button&lt;/code&gt;, 它将应用于 &lt;code&gt;StackPanel&lt;/code&gt; 下所有的 &lt;code&gt;Button&lt;/code&gt;, 例如在上面代码中, 我们为按钮定义了 &lt;code&gt;Margin&lt;/code&gt; 值为 3, 这样, 这个 &lt;code&gt;StackPanel&lt;/code&gt; 中的所有 &lt;code&gt;Button&lt;/code&gt; 都可以快速的空出一些间距, 而不需要我们为每一个 &lt;code&gt;Button&lt;/code&gt; 赋值 &lt;code&gt;Margin&lt;/code&gt; 了.&lt;/p&gt;
&lt;p&gt;效果图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/c8787655929448d88d727980e975e2ae.png&quot; alt=&quot;效果图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;很多时候, 我们也在 &lt;code&gt;Window&lt;/code&gt; 或 &lt;code&gt;Page&lt;/code&gt; 中定义一些资源, 例如转换器, 某些必要的数据之类的, 这些资源在整个 &lt;code&gt;Window&lt;/code&gt; 和 &lt;code&gt;Page&lt;/code&gt; 都能访问, 而不会被外面访问到, 非常方便.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Page x:Class=&quot;OpenGptChat.Views.MainPage&quot; Title=&quot;MainPage&quot;
      xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
      xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
      xmlns:mc=&quot;http://schemas.openxmlformats.org/markup-compatibility/2006&quot; 
      xmlns:d=&quot;http://schemas.microsoft.com/expression/blend/2008&quot; 
      xmlns:local=&quot;clr-namespace:OpenGptChat.Views&quot;
      xmlns:utilities=&quot;clr-namespace:OpenGptChat.Utilities;assembly=OpenGptChat.Common&quot;
      mc:Ignorable=&quot;d&quot; d:DesignHeight=&quot;600&quot; d:DesignWidth=&quot;880&quot; d:Background=&quot;White&quot;
      d:DataContext=&quot;{d:DesignInstance Type=local:MainPage}&quot;
      Background=&quot;{DynamicResource GeneralBackground}&quot; Style=&quot;{DynamicResource AnimatedPageStyle}&quot;&amp;gt;
    &amp;lt;Page.Resources&amp;gt;
        &amp;lt;utilities:BindingProxy x:Key=&quot;PageSelf&quot; Data=&quot;{Binding}&quot;/&amp;gt;
    &amp;lt;/Page.Resources&amp;gt;

    &amp;lt;!--省略页面内容--&amp;gt;
&amp;lt;/Page&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 在资源字典中定义资源&lt;/h3&gt;
&lt;p&gt;如果你翻了 &lt;code&gt;FrameworkElement&lt;/code&gt; 的定义, 你会发现, 它的 &lt;code&gt;Resources&lt;/code&gt; 属性的类型, 就是 &lt;code&gt;ResourceDictionary&lt;/code&gt;, 即: 资源字典.&lt;/p&gt;
&lt;p&gt;在 WPF 中, 你同样可以在你的项目中创建一个资源字典文件, 它是一个 XAML 文件, 大概是这样的格式:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ResourceDictionary xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
                    xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;&amp;gt;
                    
  &amp;lt;!--在这里定义资源--&amp;gt;
  
&amp;lt;/ResourceDictionary&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你想在别的地方引用一个资源字典, 可以直接在你的 XAML 中这么声明:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ResourceDictionary Source=&quot;资源字典的路径&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为刚刚的 &lt;code&gt;FrameworkElement&lt;/code&gt; 的 &lt;code&gt;Resources&lt;/code&gt; 属性是 &lt;code&gt;ResourceDictionary&lt;/code&gt; 类型, 所以你也可以直接把自己的资源字典赋值过去, 大概是这样的:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;StackPanel&amp;gt;
  &amp;lt;StackPanel.Resources&amp;gt;
    &amp;lt;ResourceDictionary Source=&quot;资源字典的路径&quot;/&amp;gt;
  &amp;lt;/StackPanel.Resources&amp;gt;
&amp;lt;/StackPanel&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你希望定义一些全局都能使用的资源, 那么你可以直接在项目中的 &lt;code&gt;App.xaml&lt;/code&gt; 中定义. 在 &lt;code&gt;App.xaml&lt;/code&gt; 定义的资源, 是整个程序都可以直接访问到的, 例如这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Application x:Class=&quot;WpfTutorial.App&quot;
             xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
             xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
             xmlns:local=&quot;clr-namespace:WpfTutorial&quot;
             StartupUri=&quot;MainWindow.xaml&quot;&amp;gt;
  &amp;lt;Application.Resources&amp;gt;
    &amp;lt;Style TargetType=&quot;Button&quot; x:Key=&quot;pinkButton&quot;&amp;gt;
      &amp;lt;Setter Property=&quot;Background&quot; Value=&quot;Pink&quot;/&amp;gt;
    &amp;lt;/Style&amp;gt;
  &amp;lt;/Application.Resources&amp;gt;
&amp;lt;/Application&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要知道的是, 资源字典也是可以合并的, &lt;code&gt;ResourceDictionary&lt;/code&gt; 有一个属性叫做 &lt;code&gt;MergedDictionaries&lt;/code&gt;, 你可以在这里添加一些其他的资源字典, 然后那些资源字典中包含的资源, 就会合并到当前字典中.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OpenGptChat&lt;/code&gt; 的 &lt;code&gt;App.xaml&lt;/code&gt; 是这样定义的:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Application x:Class=&quot;OpenGptChat.App&quot;
             xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
             xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
             xmlns:local=&quot;clr-namespace:OpenGptChat&quot;&amp;gt;
    &amp;lt;Application.Resources&amp;gt;
        &amp;lt;ResourceDictionary&amp;gt;
            &amp;lt;ResourceDictionary.MergedDictionaries&amp;gt;
                &amp;lt;ResourceDictionary Source=&quot;/Themes/Generic.xaml&quot;/&amp;gt;
                &amp;lt;ResourceDictionary Source=&quot;/Resources/AnimationStyles.xaml&quot;/&amp;gt;
                &amp;lt;ResourceDictionary Source=&quot;/Resources/ControlStyles/All.xaml&quot;/&amp;gt;
                &amp;lt;ResourceDictionary Source=&quot;/Converters/SingletonConverters.xaml&quot;/&amp;gt;

                &amp;lt;ResourceDictionary Source=&quot;pack://application:,,,/OpenGptChat.Common;component/ColorModes/BrightMode.xaml&quot;/&amp;gt;
                &amp;lt;ResourceDictionary Source=&quot;pack://application:,,,/OpenGptChat.Common;component/Languages/en.xaml&quot;/&amp;gt;
            &amp;lt;/ResourceDictionary.MergedDictionaries&amp;gt;
        &amp;lt;/ResourceDictionary&amp;gt;
    &amp;lt;/Application.Resources&amp;gt;
&amp;lt;/Application&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的代码中我们可以看到, 它没有直接在 &lt;code&gt;App.xaml&lt;/code&gt; 中直接定义任何资源, 而是在其他的地方定义资源, 最后将它们合并到 &lt;code&gt;App.xaml&lt;/code&gt;, 这样将资源分门别类, 管理起来也是方便的.&lt;/p&gt;
&lt;h2&gt;访问资源的方式&lt;/h2&gt;
&lt;p&gt;在上面的内容中, 你应该也看到了, 我直接在控件中定义的 &lt;code&gt;Style&lt;/code&gt;, 它有一个 &lt;code&gt;TargetType&lt;/code&gt; 属性, 或者我在上面 &lt;code&gt;App.xaml&lt;/code&gt; 中定义 &lt;code&gt;Style&lt;/code&gt; 的时候, 添加了一个 &lt;code&gt;x:Key&lt;/code&gt; 属性. 是这些决定了资源的访问方式.&lt;/p&gt;
&lt;h3&gt;1. 访问使用目标类型定义的资源&lt;/h3&gt;
&lt;p&gt;如果一个资源是一个 &lt;code&gt;Style&lt;/code&gt;, 那么它可以直接有一个 &lt;code&gt;TargetType&lt;/code&gt; 属性, 当你给它赋值后, 所有能访问到该资源的, 对应类型的控件, 都会应用这个样式, 换言之, 你可以用它定义一些 &lt;em&gt;全局&lt;/em&gt; 或 &lt;em&gt;通用&lt;/em&gt; 的样式, 然后应用到你想要的地方.&lt;/p&gt;
&lt;p&gt;上面直接在 &lt;code&gt;StackPanel&lt;/code&gt; 中定义 &lt;code&gt;Style&lt;/code&gt; 资源的例子就是使用的这种方式, 它只有一个 &lt;code&gt;TargetType&lt;/code&gt;, 于是它的成员中, 所有的 &lt;code&gt;Button&lt;/code&gt; 都会应用这个样式, 然后设置 &lt;code&gt;Margin&lt;/code&gt; 值为样式中指定的值.&lt;/p&gt;
&lt;p&gt;如果你希望将一个样式应用到整个应用程序, 很简单, 只需要让它可以被整个应用程序访问即可, 那么我们应该用什么呢? 当然是 &lt;code&gt;App.xaml&lt;/code&gt; 了. 你在 &lt;code&gt;App.xaml&lt;/code&gt; 中直接定义的 &lt;code&gt;Style&lt;/code&gt; 资源或者合并到 &lt;code&gt;App.xaml&lt;/code&gt; 的 &lt;code&gt;Style&lt;/code&gt; 资源, 如果设置了 &lt;code&gt;TargetType&lt;/code&gt; 并且没有设置 &lt;code&gt;x:Key&lt;/code&gt;, 那么它们就会对全局进行应用.&lt;/p&gt;
&lt;h3&gt;2. 访问使用键定义的资源&lt;/h3&gt;
&lt;p&gt;任何一个资源, 如果你定义了键, 也就是 &lt;code&gt;x:Key&lt;/code&gt;, 那么访问它的时候, 就只能通过 &lt;code&gt;{StaticResource 键}&lt;/code&gt; 或 &lt;code&gt;{DynamicResource 键}&lt;/code&gt; 来访问.&lt;/p&gt;
&lt;p&gt;它们分别是静态资源和动态资源. 作用就和它们的名字一样, 静态资源一旦引用之后, 就不会变更, 而使用动态资源引用的资源, 如果你动态更新了资源字典, 那么这个引用也会更新.&lt;/p&gt;
&lt;p&gt;通过这个特性, 我们可以用来做 &quot;语言国际化&quot;, 也就是给程序添加多语言支持. 同理, 如果你要做程序的颜色模式切换, 那么也可以直接做, 毕竟, 控件的 &lt;code&gt;Foreground&lt;/code&gt;, &lt;code&gt;Background&lt;/code&gt; 以及几乎任何东西都是可以通过动态资源来引用的.&lt;/p&gt;
&lt;h2&gt;动态切换资源&lt;/h2&gt;
&lt;p&gt;只要你的 CS 代码能访问到的地方, 你都可以操作. 以 &lt;code&gt;App.xaml&lt;/code&gt; 为例, 它其实是一个类, 基类型是 &lt;code&gt;Application&lt;/code&gt;, 而我们在 &lt;code&gt;App.xaml&lt;/code&gt; 中访问到的属性, 基本都是在访问 &lt;code&gt;Application&lt;/code&gt; 的属性.&lt;/p&gt;
&lt;h3&gt;1. 基本操作&lt;/h3&gt;
&lt;p&gt;如果你翻过 &lt;code&gt;Application&lt;/code&gt; 的定义, 你会发现, 它提供了一个 &lt;code&gt;Current&lt;/code&gt; 属性, 用来获取当前正在运行的 &lt;code&gt;Application&lt;/code&gt; 实例.&lt;/p&gt;
&lt;p&gt;下面是操作 &lt;code&gt;Application&lt;/code&gt; 替换当前程序的资源的示例代码.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ResourceDictionary res = 我的资源字典, 这里怎么获取到的你自己决定咯;

// 直接将当前应用程序的资源替换成我们的资源
Application.Current.Resources = res;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者委婉一点, 我们也可以在当前程序资源字典中的 &lt;code&gt;MergedDictionaries&lt;/code&gt; 中添加自己的资源, 它可以直接覆盖掉相同 &lt;code&gt;x:Key&lt;/code&gt; 的值.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ResourceDictionary res = 我的资源字典, 这里怎么获取的你自己决定咯;

// 在当前应用程序的资源中, 添加我们自己的资源.
Application.Current.Resources.MergedDictionaries.Add(res);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 删掉旧的, 添加新的&lt;/h3&gt;
&lt;p&gt;直接替换掉程序的资源字典显然不好, 因为我们里面可能定义了很多东西, 更换起来不方便. 而直接向里面添加新的, 覆盖旧的也不好, 因为久而久之, 资源字典会越积越多, 可能会影响程序的性能.&lt;/p&gt;
&lt;p&gt;想要删去旧的字典, 首先我们就得知道这个字典是不是我们所要删去的字典. 例如我程序资源的合并字典中有一个用来存放 &lt;em&gt;语言&lt;/em&gt; 的字典, 那么做得让它有个地方能够标识出 &quot;这个字典是用来存放语言信息的&quot;. 最简单的方式就是在字典中加入一个带有标识性质的资源, 例如这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ResourceDictionary xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
                    xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
                    xmlns:s=&quot;clr-namespace:System;assembly=mscorlib&quot;&amp;gt;
    
    &amp;lt;!-- 这里存放资源 --&amp;gt;
    
    &amp;lt;!-- 用一个名称不会和其他资源字典资源重复的任意资源做标识 --&amp;gt;
    &amp;lt;s:Boolean x:Key=&quot;IsLanguageResource&quot;&amp;gt;True&amp;lt;/s:Boolean&amp;gt;
&amp;lt;/ResourceDictionary&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在判断的时候这样写:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var oldResources = 
    Application.Current.Resources.MergedDictionaries
        .Where(res =&amp;gt; res.Contains(&quot;IsLanguageResource&quot;)
        .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面的代码取自 &lt;a href=&quot;https://github.com/SlimeNull/OpenGptChat&quot;&gt;OpenGptChat&lt;/a&gt; 的 &lt;code&gt;ColorModeService&lt;/code&gt;, 可以做动态资源切换的参考:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 切换资源到指定资源字典
private void SwitchTo(ResourceDictionary colorModeResource)
{
    // 拿到旧的资源
    var oldColorModeResources =
        Application.Current.Resources.MergedDictionaries
            .Where(dict =&amp;gt; dict.Contains(&quot;IsColorModeResource&quot;))
            .ToList();

    // 删掉旧的资源
    foreach (var res in oldColorModeResources)
        Application.Current.Resources.MergedDictionaries.Remove(res);

    // 添加新的资源
    Application.Current.Resources.MergedDictionaries.Add(colorModeResource);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;国际化与配色切换&lt;/h2&gt;
&lt;p&gt;WPF 中的界面可以绑定到资源, 而资源切换后界面也会跟着更新, 利用这个特点, 我们可以将要显示的文本绑定到我们的语言资源, 将界面中前景色, 背景色等各种控件的颜色绑定到我们的配色资源, 然后动态切换资源, 即可做到国际化与配色切换.&lt;/p&gt;
&lt;h3&gt;1. 编写多个资源文件&lt;/h3&gt;
&lt;p&gt;做国际化的时候, 为了避免资源找不到的情况, 我们还需要一个 &lt;em&gt;fallback&lt;/em&gt; 的情况. 因为资源是可覆盖的, 所以我们使用以下逻辑:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定义一个 &lt;code&gt;_base.xaml&lt;/code&gt;, 在里面编写一些基础的资源, 以及默认语言的一些文本.&lt;/li&gt;
&lt;li&gt;定义其他语言, 例如 &lt;code&gt;zh-Hans.xaml&lt;/code&gt;, 然后在 &lt;code&gt;MergedDictionaries&lt;/code&gt; 中添加 &lt;code&gt;_base.xaml&lt;/code&gt;, 这样, 雀食的内容会 &lt;em&gt;fallback&lt;/em&gt; 到 &lt;code&gt;_base.xaml&lt;/code&gt; 中的资源, 而重复的资源则会进行覆盖.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面是来自 &lt;a href=&quot;https://github.com/SlimeNull/OpenGptChat&quot;&gt;OpenGptChat&lt;/a&gt; 的, 可参考的  &lt;code&gt;_base.xaml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ResourceDictionary xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
                    xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
                    xmlns:s=&quot;clr-namespace:System;assembly=mscorlib&quot;&amp;gt;
    
    &amp;lt;!--页面名--&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrChat&quot;&amp;gt;Chat&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrConfiguration&quot;&amp;gt;Configuration&amp;lt;/s:String&amp;gt;

    &amp;lt;!--配置名--&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrAPIKey&quot;&amp;gt;API Key&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrAPIGPTModel&quot;&amp;gt;API GPT Model&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrAPIHost&quot;&amp;gt;API Host&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrAPITimeout&quot;&amp;gt;API Timeout&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrTemperature&quot;&amp;gt;Temperature&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrSystemMessages&quot;&amp;gt;System messages&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrApply&quot;&amp;gt;Apply&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrWindowAlwaysOnTop&quot;&amp;gt;Window always on top&amp;lt;/s:String&amp;gt;

    &amp;lt;s:String x:Key=&quot;StrLanguage&quot;&amp;gt;Language&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrColorMode&quot;&amp;gt;Color mode&amp;lt;/s:String&amp;gt;

    &amp;lt;!--热键描述--&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrHotkeyTips&quot;&amp;gt;Hotkety Tips&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrCloseApplication&quot;&amp;gt;Close application&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrHideApplication&quot;&amp;gt;Hide application&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrShowApplication&quot;&amp;gt;Show application&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrSendMessage&quot;&amp;gt;Send message&amp;lt;/s:String&amp;gt;

    &amp;lt;s:String x:Key=&quot;StrGlobalHotkey&quot;&amp;gt;Global hotkey&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrInputBox&quot;&amp;gt;Input box&amp;lt;/s:String&amp;gt;

    &amp;lt;!--按钮--&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrSend&quot;&amp;gt;Send&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrSave&quot;&amp;gt;Save&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrNewSession&quot;&amp;gt;New Session&amp;lt;/s:String&amp;gt;

    &amp;lt;!--工具提示--&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrAboutOpenGptChat&quot;&amp;gt;About OpenGptChat&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrGoBackToMainPage&quot;&amp;gt;Go back to Main page&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrResetChat&quot;&amp;gt;Reset chat&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrGoToConfigurationPage&quot;&amp;gt;Go to Configuration page&amp;lt;/s:String&amp;gt;
    
    &amp;lt;!--菜单相关--&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrCopy&quot;&amp;gt;Copy&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrEdit&quot;&amp;gt;Edit&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrDelete&quot;&amp;gt;Delete&amp;lt;/s:String&amp;gt;
    
    &amp;lt;!--最后使用一个 &apos;IsLanguageResource&apos; 标识当前资源字典是语言资源--&amp;gt;
    &amp;lt;s:Boolean x:Key=&quot;IsLanguageResource&quot;&amp;gt;True&amp;lt;/s:Boolean&amp;gt;
&amp;lt;/ResourceDictionary&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到, 上面的内容中, 我们使用的是英文, 所以当我们编写 &lt;code&gt;en.xaml&lt;/code&gt; 的时候, 只需要直接 &lt;em&gt;fallback&lt;/em&gt;, 而不需要重新写内容:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ResourceDictionary xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
                    xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
                    xmlns:s=&quot;clr-namespace:System;assembly=mscorlib&quot;&amp;gt;
    &amp;lt;ResourceDictionary.MergedDictionaries&amp;gt;
        &amp;lt;ResourceDictionary Source=&quot;_base.xaml&quot;/&amp;gt;
    &amp;lt;/ResourceDictionary.MergedDictionaries&amp;gt;

    &amp;lt;!--英语是默认语言, 直接 fallback 过去--&amp;gt;
    
&amp;lt;/ResourceDictionary&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是, 如果是中文的话, 我们就需要进行覆盖了:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ResourceDictionary xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
                    xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
                    xmlns:s=&quot;clr-namespace:System;assembly=mscorlib&quot;&amp;gt;
    &amp;lt;ResourceDictionary.MergedDictionaries&amp;gt;
        &amp;lt;ResourceDictionary Source=&quot;_base.xaml&quot;/&amp;gt;
    &amp;lt;/ResourceDictionary.MergedDictionaries&amp;gt;
    
    &amp;lt;!--页面名称--&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrChat&quot;&amp;gt;聊天&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrConfiguration&quot;&amp;gt;配置&amp;lt;/s:String&amp;gt;

    &amp;lt;!--配置名称--&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrAPIKey&quot;&amp;gt;API 密钥&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrAPIHost&quot;&amp;gt;API 主机&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrAPIGPTModel&quot;&amp;gt;API GPT 模型&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrAPITimeout&quot;&amp;gt;API 超时时间&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrTemperature&quot;&amp;gt;温度&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrSystemMessages&quot;&amp;gt;系统消息&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrApply&quot;&amp;gt;应用&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrWindowAlwaysOnTop&quot;&amp;gt;窗口置顶&amp;lt;/s:String&amp;gt;

    &amp;lt;s:String x:Key=&quot;StrLanguage&quot;&amp;gt;语言&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrColorMode&quot;&amp;gt;色彩模式&amp;lt;/s:String&amp;gt;

    &amp;lt;!--热键描述--&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrHotkeyTips&quot;&amp;gt;热键提示&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrCloseApplication&quot;&amp;gt;关闭程序&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrHideApplication&quot;&amp;gt;隐藏程序&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrShowApplication&quot;&amp;gt;显示程序&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrSendMessage&quot;&amp;gt;发送消息&amp;lt;/s:String&amp;gt;

    &amp;lt;s:String x:Key=&quot;StrGlobalHotkey&quot;&amp;gt;全局热键&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrInputBox&quot;&amp;gt;输入框&amp;lt;/s:String&amp;gt;


    &amp;lt;!--按钮--&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrSend&quot;&amp;gt;发送&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrSave&quot;&amp;gt;保存&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrNewSession&quot;&amp;gt;新建会话&amp;lt;/s:String&amp;gt;

    &amp;lt;!--工具提示--&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrAboutOpenGptChat&quot;&amp;gt;关于 OpenGptChat&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrGoBackToMainPage&quot;&amp;gt;返回到主页面&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrResetChat&quot;&amp;gt;重置聊天&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrGoToConfigurationPage&quot;&amp;gt;转到配置页面&amp;lt;/s:String&amp;gt;

    &amp;lt;!--菜单--&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrCopy&quot;&amp;gt;复制&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrEdit&quot;&amp;gt;编辑&amp;lt;/s:String&amp;gt;
    &amp;lt;s:String x:Key=&quot;StrDelete&quot;&amp;gt;删除&amp;lt;/s:String&amp;gt;
&amp;lt;/ResourceDictionary&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 编写资源切换的逻辑&lt;/h3&gt;
&lt;p&gt;按照我们刚刚所说的逻辑, 只需要稍加完善, 即可得到下面用于语言切换的代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class LanguageService
{
    private static string resourceUriPrefix = &quot;pack://application:,,,/OpenGptChat.Common;component&quot;;

    private static Dictionary&amp;lt;CultureInfo, ResourceDictionary&amp;gt; languageResources =
        new Dictionary&amp;lt;CultureInfo, ResourceDictionary&amp;gt;()
        {
            { new CultureInfo(&quot;en&quot;), new ResourceDictionary() { Source = new Uri($&quot;{resourceUriPrefix}/Languages/en.xaml&quot; ) } },
            { new CultureInfo(&quot;zh-hans&quot;), new ResourceDictionary() { Source = new Uri($&quot;{resourceUriPrefix}/Languages/zh-hans.xaml&quot; ) } },
            { new CultureInfo(&quot;zh-hant&quot;), new ResourceDictionary() { Source = new Uri($&quot;{resourceUriPrefix}/Languages/zh-hant.xaml&quot; ) } },
            { new CultureInfo(&quot;ja&quot;), new ResourceDictionary() { Source = new Uri($&quot;{resourceUriPrefix}/Languages/ja.xaml&quot; ) } },
            { new CultureInfo(&quot;ar&quot;), new ResourceDictionary() { Source = new Uri($&quot;{resourceUriPrefix}/Languages/ar.xaml&quot; ) } },
            { new CultureInfo(&quot;es&quot;), new ResourceDictionary() { Source = new Uri($&quot;{resourceUriPrefix}/Languages/es.xaml&quot; ) } },
            { new CultureInfo(&quot;fr&quot;), new ResourceDictionary() { Source = new Uri($&quot;{resourceUriPrefix}/Languages/fr.xaml&quot; ) } },
            { new CultureInfo(&quot;ru&quot;), new ResourceDictionary() { Source = new Uri($&quot;{resourceUriPrefix}/Languages/ru.xaml&quot; ) } },
            { new CultureInfo(&quot;ur&quot;), new ResourceDictionary() { Source = new Uri($&quot;{resourceUriPrefix}/Languages/ur.xaml&quot; ) } },
            { new CultureInfo(&quot;tr&quot;), new ResourceDictionary() { Source = new Uri($&quot;{resourceUriPrefix}/Languages/tr.xaml&quot; ) } },
        };

    private static CultureInfo defaultLanguage =
        new CultureInfo(&quot;en&quot;);

    public LanguageService(
        ConfigurationService configurationService)
    {
        // 如果配置文件里面有指定语言, 则设置语言
        CultureInfo language = CultureInfo.CurrentCulture;
        if (!string.IsNullOrWhiteSpace(configurationService.Configuration.Language))
            language = new CultureInfo(configurationService.Configuration.Language);

        SetLanguage(language);
    }
    
    private CultureInfo currentLanguage =
        defaultLanguage;

    public IEnumerable&amp;lt;CultureInfo&amp;gt; Languages =&amp;gt;
        languageResources.Keys;

    public CultureInfo CurrentLanguage
    {
        get =&amp;gt; currentLanguage;
        set
        {
            if (!SetLanguage(value))
                throw new ArgumentException(&quot;Unsupport language&quot;);
        }
    }
    
    public bool SetLanguage(CultureInfo language)
    {
        // 查找一个合适的 key (先查找完全符合的语言)
        CultureInfo? key = Languages
            .Where(key =&amp;gt; key.Equals(language))
            .FirstOrDefault();
            
        // 如果没找到, 就降低要求, 找语言相同的语言
        // 例如资源中只有美式英语, 但是需要切换到英式英语
        // 因为这俩是同一个语言只是地区不同, 所以也能切换过去
        if (key == null)
            key = Languages
                .Where(key =&amp;gt; key.TwoLetterISOLanguageName == language.TwoLetterISOLanguageName)
                .FirstOrDefault();

        if (key != null)
        {
            // 取资源字典
            ResourceDictionary? resourceDictionary = languageResources[key];

            // 取旧的资源
            var oldLanguageResources =
                Application.Current.Resources.MergedDictionaries
                    .Where(dict =&amp;gt; dict.Contains(&quot;IsLanguageResource&quot;))
                    .ToList();
            //删去旧的字典
            foreach (var res in oldLanguageResources)
                Application.Current.Resources.MergedDictionaries.Remove(res);

            // 添加新的资源
            Application.Current.Resources.MergedDictionaries.Add(resourceDictionary);

            currentLanguage = key;
            return true;
        }

        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 原理相同的配色切换&lt;/h3&gt;
&lt;p&gt;配色切换也是一样的逻辑, 也使用一个 Key 来标识当前资源是 &quot;配色&quot; 资源, 切换的时候也是如此, 拿到旧的资源, 删掉它们, 然后添加新的资源.&lt;/p&gt;
&lt;h2&gt;结语:&lt;/h2&gt;
&lt;p&gt;这些都是我在编写 &lt;a href=&quot;https://github.com/SlimeNull/OpenGptChat&quot;&gt;OpenGptChat&lt;/a&gt; 时总结出来的用法, 想学习 WPF 建议去看一下这个项目.&lt;/p&gt;
</content:encoded></item><item><title>[WPF] 多页面程序基本跳转</title><link>https://slimenull.com/posts/20230401210013/</link><guid isPermaLink="true">https://slimenull.com/posts/20230401210013/</guid><description>使用 WPF 实现较为便捷的多页面跳转.</description><pubDate>Sat, 01 Apr 2023 21:00:13 GMT</pubDate><content:encoded>&lt;p&gt;多页面程序是一种很常见的设计, 一个程序中有多个页面, 然后直接切换页面而不需要创建新的窗口, WPF 中使用 &lt;code&gt;Frame&lt;/code&gt; 来做页面跳转, 但是如何优雅的设计一个多页面程序, 这是个问题.&lt;/p&gt;
&lt;h2&gt;最基本的页面跳转&lt;/h2&gt;
&lt;p&gt;编写一个最基本的页面跳转, 首先我们需要在主窗口中放一个 &lt;code&gt;Frame&lt;/code&gt; 控件, 然后再编写两个 &lt;code&gt;Page&lt;/code&gt;. 下面我们以两个页面 &lt;em&gt;MainPage&lt;/em&gt; 和 &lt;em&gt;Configuration&lt;/em&gt; 作为示例讲解.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--窗口 UI--&amp;gt;
&amp;lt;Window x:Class=&quot;WpfNavigationTutorial.MainWindow&quot;
        xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
        xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
        xmlns:d=&quot;http://schemas.microsoft.com/expression/blend/2008&quot;
        xmlns:mc=&quot;http://schemas.openxmlformats.org/markup-compatibility/2006&quot;
        xmlns:local=&quot;clr-namespace:WpfNavigationTutorial&quot;
        xmlns:pages=&quot;clr-namespace:WpfTutorial.Views.Pages&quot;
        mc:Ignorable=&quot;d&quot;
        Title=&quot;MainWindow&quot; Height=&quot;450&quot; Width=&quot;800&quot;&amp;gt;
    &amp;lt;Grid&amp;gt;
        &amp;lt;Grid.ColumnDefinitions&amp;gt;
            &amp;lt;ColumnDefinition Width=&quot;3*&quot;/&amp;gt;
            &amp;lt;ColumnDefinition Width=&quot;13*&quot;/&amp;gt;
        &amp;lt;/Grid.ColumnDefinitions&amp;gt;
        
        &amp;lt;ListBox Name=&quot;navMenu&quot; SelectionChanged=&quot;ListBox_SelectionChanged&quot;&amp;gt;
            &amp;lt;ListBoxItem Content=&quot;Main&quot; Tag=&quot;{x:Type pages:MainPage}&quot;/&amp;gt;
            &amp;lt;ListBoxItem Content=&quot;Configuration&quot; Tag=&quot;{x:Type pages:ConfigurationPage}&quot;/&amp;gt;
        &amp;lt;/ListBox&amp;gt;

        &amp;lt;Frame Grid.Column=&quot;1&quot; Name=&quot;appFrame&quot; NavigationUIVisibility=&quot;Hidden&quot;/&amp;gt;
    &amp;lt;/Grid&amp;gt;
&amp;lt;/Window&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的代码中, 我们使用了一个左右两栏布局, 左侧放了一个 &lt;code&gt;ListBox&lt;/code&gt;, 右侧放了一个 &lt;code&gt;Frame&lt;/code&gt;, 在 &lt;code&gt;ListBox&lt;/code&gt; 中, 我们放了两个 &lt;code&gt;ListBoxItem&lt;/code&gt;, 使用 &lt;code&gt;Content&lt;/code&gt; 做内容, 并且给加了 &lt;code&gt;Tag&lt;/code&gt; 标识这个按钮具体要跳转到哪个页面.&lt;/p&gt;
&lt;p&gt;然后在两个页面中分别这么简单写一下内容:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--这里是主页面--&amp;gt;
&amp;lt;Page x:Class=&quot;WpfTutorial.Views.Pages.MainPage&quot;
      xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
      xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
      xmlns:mc=&quot;http://schemas.openxmlformats.org/markup-compatibility/2006&quot; 
      xmlns:d=&quot;http://schemas.microsoft.com/expression/blend/2008&quot; 
      xmlns:local=&quot;clr-namespace:WpfTutorial.Views.Pages&quot;
      mc:Ignorable=&quot;d&quot; d:DesignHeight=&quot;450&quot; d:DesignWidth=&quot;800&quot;
      Title=&quot;MainPage&quot;&amp;gt;

    &amp;lt;Grid&amp;gt;
        &amp;lt;Grid MaxWidth=&quot;400&quot; Margin=&quot;20&quot;&amp;gt;
            &amp;lt;StackPanel HorizontalAlignment=&quot;Stretch&quot;&amp;gt;
                &amp;lt;TextBlock&amp;gt;This is main page&amp;lt;/TextBlock&amp;gt;
            &amp;lt;/StackPanel&amp;gt;
        &amp;lt;/Grid&amp;gt;
    &amp;lt;/Grid&amp;gt;
&amp;lt;/Page&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--这里是里一个页面页面 (配置页面)--&amp;gt;
&amp;lt;Page x:Class=&quot;WpfTutorial.Views.Pages.ConfigurationPage&quot;
      xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
      xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
      xmlns:mc=&quot;http://schemas.openxmlformats.org/markup-compatibility/2006&quot; 
      xmlns:d=&quot;http://schemas.microsoft.com/expression/blend/2008&quot; 
      xmlns:local=&quot;clr-namespace:WpfTutorial.Views.Pages&quot;
      mc:Ignorable=&quot;d&quot; d:DesignHeight=&quot;450&quot; d:DesignWidth=&quot;800&quot;
      Title=&quot;ConfigurationPage&quot;&amp;gt;

    &amp;lt;Grid&amp;gt;
        &amp;lt;Grid MaxWidth=&quot;400&quot; Margin=&quot;20&quot;&amp;gt;
            &amp;lt;StackPanel HorizontalAlignment=&quot;Stretch&quot;&amp;gt;
                &amp;lt;TextBlock&amp;gt;This is configuration page&amp;lt;/TextBlock&amp;gt;
            &amp;lt;/StackPanel&amp;gt;
        &amp;lt;/Grid&amp;gt;
    &amp;lt;/Grid&amp;gt;
&amp;lt;/Page&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在后台代码中, 我们这么写, 当选择项变更的时候, 也使用 &lt;code&gt;Frame&lt;/code&gt; 进行页面跳转.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    // 页面实例的缓存
    private static readonly Dictionary&amp;lt;Type, Page&amp;gt; bufferedPages =
        new Dictionary&amp;lt;Type, Page&amp;gt;();

    // 当
    private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        // 如果选择项不是 ListBoxItem, 则返回
        if (navMenu.SelectedItem is not ListBoxItem item)
            return;

        // 如果 Tag 不是一个类型, 则返回
        if (item.Tag is not Type type)
            return;

        // 如果页面缓存中找不到页面, 则创建一个新的页面并存入
        if (!bufferedPages.TryGetValue(type, out Page? page))
            page = bufferedPages[type] = 
                Activator.CreateInstance(type) as Page ?? throw new Exception(&quot;this would never happen&quot;);

        // 使用 Frame 进行导航.
        appFrame.Navigate(page);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后的效果就是这样, 点击左侧项目的时候, 右侧页面会直接进行跳转:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/77d72828e9fc4e58948aaee80a267ece.png&quot; alt=&quot;预览效果&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;加一点美化&lt;/h2&gt;
&lt;p&gt;用 ListBox 做侧边页面栏就太丑了, 我们可以使用 &lt;code&gt;ListBox&lt;/code&gt; 自己给它套一个样式.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里不使用 &lt;code&gt;ItemsControl&lt;/code&gt; 是因为, &lt;code&gt;ListBox&lt;/code&gt; 的项目是 &lt;em&gt;&quot;可选中&quot;&lt;/em&gt; 的, 而 &lt;code&gt;ItemsControl&lt;/code&gt; 是没有的, &lt;code&gt;ItemsControl&lt;/code&gt; 只适合界面控件的批量绑定生成.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;创建一个 &lt;code&gt;NavigationItem&lt;/code&gt; 用来存放导航数据:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class NavigationItem
{
    /// &amp;lt;summary&amp;gt;
    /// 导航标题
    /// &amp;lt;/summary&amp;gt;
    public string Title { get; set; } = string.Empty;

    /// &amp;lt;summary&amp;gt;
    /// 导航描述
    /// &amp;lt;/summary&amp;gt;
    public string Description { get; set; } = string.Empty;

    /// &amp;lt;summary&amp;gt;
    /// 目标页面的类型
    /// &amp;lt;/summary&amp;gt;
    public Type TargetPageType { get; set; } = typeof(Page);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为 &lt;code&gt;MainWindow&lt;/code&gt; 创建一个 &lt;code&gt;MainWindowModel&lt;/code&gt; 用来存放页面数据:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MainWndowModel
{
    public ObservableCollection&amp;lt;NavigationItem&amp;gt; NavigationItems { get; } =
        new ObservableCollection&amp;lt;NavigationItem&amp;gt;()
        {
            new NavigationItem()
            {
                Title = &quot;Main&quot;,
                Description = &quot;The main page of this application&quot;,

                TargetPageType = typeof(MainPage)
            },
            new NavigationItem()
            {
                Title = &quot;Configuration&quot;,
                Description = &quot;Configure something you want&quot;,

                TargetPageType = typeof(ConfigurationPage)
            }
        };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后将主页面的代码改成这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Window x:Class=&quot;WpfNavigationTutorial.MainWindow&quot;
        xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
        xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
        xmlns:d=&quot;http://schemas.microsoft.com/expression/blend/2008&quot;
        xmlns:mc=&quot;http://schemas.openxmlformats.org/markup-compatibility/2006&quot;
        xmlns:local=&quot;clr-namespace:WpfNavigationTutorial&quot;
        xmlns:pages=&quot;clr-namespace:WpfTutorial.Views.Pages&quot;
        xmlns:vm=&quot;clr-namespace:WpfNavigationTutorial.ViewModels&quot;
        mc:Ignorable=&quot;d&quot;
        Title=&quot;MainWindow&quot; Height=&quot;450&quot; Width=&quot;800&quot;&amp;gt;
  &amp;lt;Window.DataContext&amp;gt;
    &amp;lt;!--直接在 XAML 中给 Window 的 DataContext 赋值--&amp;gt;
    &amp;lt;vm:MainWndowModel/&amp;gt;
  &amp;lt;/Window.DataContext&amp;gt;

  &amp;lt;Grid&amp;gt;
    &amp;lt;Grid.ColumnDefinitions&amp;gt;
      &amp;lt;ColumnDefinition Width=&quot;3*&quot;/&amp;gt;
      &amp;lt;ColumnDefinition Width=&quot;13*&quot;/&amp;gt;
    &amp;lt;/Grid.ColumnDefinitions&amp;gt;

    &amp;lt;!--使用 ListBox, 并且将 ItemsSource 绑定到我们的数据--&amp;gt;
    &amp;lt;!--记得要使用禁用它的水平滚动条--&amp;gt;
    &amp;lt;ListBox Name=&quot;navMenu&quot;
             ItemsSource=&quot;{Binding NavigationItems}&quot; BorderThickness=&quot;0 0 1 0&quot;
             ScrollViewer.HorizontalScrollBarVisibility=&quot;Disabled&quot;
             SelectionChanged=&quot;navMenu_SelectionChanged&quot;&amp;gt;
      &amp;lt;ListBox.ItemTemplate&amp;gt;
        &amp;lt;DataTemplate&amp;gt;
          &amp;lt;Border Padding=&quot;5&quot;&amp;gt;
            &amp;lt;StackPanel&amp;gt;
              &amp;lt;TextBlock Text=&quot;{Binding Title}&quot;/&amp;gt;

              &amp;lt;!--描述信息用灰色字体, 字号也小一点, 多余的部分剪切掉--&amp;gt;
              &amp;lt;TextBlock Text=&quot;{Binding Description}&quot; Foreground=&quot;Gray&quot;
                         FontSize=&quot;10&quot; TextTrimming=&quot;CharacterEllipsis&quot;/&amp;gt;
            &amp;lt;/StackPanel&amp;gt;
          &amp;lt;/Border&amp;gt;
        &amp;lt;/DataTemplate&amp;gt;
      &amp;lt;/ListBox.ItemTemplate&amp;gt;
    &amp;lt;/ListBox&amp;gt;

    &amp;lt;Frame Grid.Column=&quot;1&quot; Name=&quot;appFrame&quot; NavigationUIVisibility=&quot;Hidden&quot;/&amp;gt;
  &amp;lt;/Grid&amp;gt;
&amp;lt;/Window&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后台代码也相应的改一下, 只需要判断数据是否是 &lt;code&gt;NavigationItem&lt;/code&gt; 就好:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private static readonly Dictionary&amp;lt;Type, Page&amp;gt; bufferedPages =
        new Dictionary&amp;lt;Type, Page&amp;gt;();

    private void navMenu_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        // 如果选择项不是 FrameworkElement, 则返回
        if (navMenu.SelectedItem is not NavigationItem item)
            return;

        Type type =
            item.TargetPageType;

        // 如果页面缓存中找不到页面, 则创建一个新的页面并存入
        if (!bufferedPages.TryGetValue(type, out Page? page))
            page = bufferedPages[type] =
                Activator.CreateInstance(type) as Page ?? throw new Exception(&quot;this would never happen&quot;);

        appFrame.Navigate(page);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后的效果:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/369cf51132824f63a3d621731d0ac32b.png&quot; alt=&quot;稍微美化一点&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当然, 这样的效果还是很简单, 不过, 更复杂的设计, 就任由你自己发挥啦. 例如改掉 &lt;code&gt;ListBox&lt;/code&gt; 元素被选中时的丑陋颜色, 以及去掉聚集焦点时的虚线, 或者加一些动画, 都可以.&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>[WPF] 集合元素数据绑定与模板</title><link>https://slimenull.com/posts/20230401165316/</link><guid isPermaLink="true">https://slimenull.com/posts/20230401165316/</guid><description>在 WPF 中使用 ItemsControl 进行集合数据的绑定</description><pubDate>Sat, 01 Apr 2023 16:53:16 GMT</pubDate><content:encoded>&lt;p&gt;在我们的应用程序里面, 很多时候会用到列表, 而列表元素的样式, 我们是希望能够自定义的, 根据 MVVM 设计模式, 我们的列表元素, 也需要绑定到后台数据, 这样后台数据进行更新的时候, 前台 UI 也会跟着更新, 包括元素也会自动增删.&lt;/p&gt;
&lt;h2&gt;使用 ItemsControl&lt;/h2&gt;
&lt;p&gt;ItemsControl 是最基本的 &quot;多元素&quot; 控件, 像是 ListBox, ListView 他都是基于 ItemsControl 的, 因为 ListBox 和 ListView 它们有一些默认样式. 例如 ListBox 它有一个滚动条, 所以当我们自定义的时候, 还是推荐使用 ItemsControl.&lt;/p&gt;
&lt;p&gt;首先编写一个简单的数据模型:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class StudentInfo
{
    public string Name { get; set; }
    public string Description { get; set; }

    public int Age { get; set; }
    public string Gender { get; set; }

    public int Grade { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再写一个用于绑定数据的 &lt;strong&gt;ViewModel&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MainWindowModel
{
    public ObservableCollection&amp;lt;StudentInfo&amp;gt; StudentInfos { get; } = new ObservableCollection&amp;lt;StudentInfo&amp;gt;()
    {
        new StudentInfo()
        {
            Name = &quot;IFdream&quot;,
            Description = &quot;上课不听讲的学生&quot;,

            Age = 17,
            Gender = &quot;Female&quot;,
            Grade = 114514
        },

        new StudentInfo()
        {
            Name = &quot;ilyFairy&quot;,
            Description = &quot;哼哼哼啊啊啊的学生&quot;,

            Age = 21,
            Gender = &quot;武装直升机&quot;,
            Grade = 1919810
        }
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在窗口中随便写一个简单的界面:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Window x:Class=&quot;WpfItemsBindingTutorial.MainWindow&quot;
        xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
        xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
        xmlns:d=&quot;http://schemas.microsoft.com/expression/blend/2008&quot;
        xmlns:mc=&quot;http://schemas.openxmlformats.org/markup-compatibility/2006&quot;
        xmlns:vm=&quot;clr-namespace:WpfItemsBindingTutorial.ViewModels&quot;
        xmlns:local=&quot;clr-namespace:WpfItemsBindingTutorial&quot;
        mc:Ignorable=&quot;d&quot;
        Title=&quot;MainWindow&quot; Height=&quot;450&quot; Width=&quot;800&quot;&amp;gt;
  &amp;lt;Window.DataContext&amp;gt;
    &amp;lt;!--直接给窗体的 DataContext 赋值--&amp;gt;
    &amp;lt;vm:MainWindowModel/&amp;gt;
  &amp;lt;/Window.DataContext&amp;gt;
  &amp;lt;Grid&amp;gt;
    &amp;lt;Grid Margin=&quot;20&quot; MaxWidth=&quot;400&quot;&amp;gt;
      &amp;lt;StackPanel&amp;gt;

        &amp;lt;!--使用 ItemsControl 进行集合的数据绑定, 在 ItemsSource 里面指定我们的集合--&amp;gt;
        &amp;lt;ItemsControl ItemsSource=&quot;{Binding StudentInfos}&quot;&amp;gt;
          &amp;lt;ItemsControl.ItemTemplate&amp;gt;

            &amp;lt;!--使用数据模板就可以为每一个元素自定义呈现方式了--&amp;gt;
            &amp;lt;DataTemplate&amp;gt;

              &amp;lt;!--整一个边框, 加背景颜色, 圆角, 以及内外边距--&amp;gt;
              &amp;lt;Border Margin=&quot;0 10 0 0&quot; Padding=&quot;5&quot; Background=&quot;#eeeeee&quot; CornerRadius=&quot;5&quot;&amp;gt;

                &amp;lt;!--用 Effect 加个阴影--&amp;gt;
                &amp;lt;Border.Effect&amp;gt;
                  &amp;lt;DropShadowEffect ShadowDepth=&quot;0&quot; BlurRadius=&quot;3&quot;/&amp;gt;
                &amp;lt;/Border.Effect&amp;gt;

                &amp;lt;!--这里用栈布局来做主要内容--&amp;gt;
                &amp;lt;StackPanel&amp;gt;

                  &amp;lt;!--在数据模板中, 可以直接绑定到我们的数据成员--&amp;gt;
                  &amp;lt;TextBlock&amp;gt;
                    &amp;lt;Run Text=&quot;Name:&quot;/&amp;gt;
                    &amp;lt;Run Text=&quot;{Binding Name}&quot;/&amp;gt;
                  &amp;lt;/TextBlock&amp;gt;
                  &amp;lt;TextBlock&amp;gt;
                    &amp;lt;Run Text=&quot;Description:&quot;/&amp;gt;
                    &amp;lt;Run Text=&quot;{Binding Description}&quot;/&amp;gt;
                  &amp;lt;/TextBlock&amp;gt;

                  &amp;lt;!--用 UniformGrid 做个三栏布局--&amp;gt;
                  &amp;lt;UniformGrid Columns=&quot;3&quot;&amp;gt;
                    &amp;lt;TextBlock&amp;gt;
                      &amp;lt;Run Text=&quot;Age:&quot;/&amp;gt;
                      &amp;lt;Run Text=&quot;{Binding Age}&quot;/&amp;gt;
                    &amp;lt;/TextBlock&amp;gt;
                    &amp;lt;TextBlock&amp;gt;
                      &amp;lt;Run Text=&quot;Gender:&quot;/&amp;gt;
                      &amp;lt;Run Text=&quot;{Binding Gender}&quot;/&amp;gt;
                    &amp;lt;/TextBlock&amp;gt;
                    &amp;lt;TextBlock&amp;gt;
                      &amp;lt;Run Text=&quot;Grade:&quot;/&amp;gt;
                      &amp;lt;Run Text=&quot;{Binding Grade}&quot;/&amp;gt;
                    &amp;lt;/TextBlock&amp;gt;
                  &amp;lt;/UniformGrid&amp;gt;
                &amp;lt;/StackPanel&amp;gt;
              &amp;lt;/Border&amp;gt;
            &amp;lt;/DataTemplate&amp;gt;
          &amp;lt;/ItemsControl.ItemTemplate&amp;gt;
        &amp;lt;/ItemsControl&amp;gt;
      &amp;lt;/StackPanel&amp;gt;
    &amp;lt;/Grid&amp;gt;
  &amp;lt;/Grid&amp;gt;
&amp;lt;/Window&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是我们就得到了这样的一个页面:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/2c5aa4a7c76b4a1a910bf415c1d429b0.png&quot; alt=&quot;效果预览&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;使用 ListBox&lt;/h2&gt;
&lt;p&gt;事实上, &lt;code&gt;ListBox&lt;/code&gt; 的使用和 &lt;code&gt;ItemsControl&lt;/code&gt; 是差不多的, 包括同样的方式绑定 &lt;code&gt;ItemsSource&lt;/code&gt;, 同样的方式设置 &lt;code&gt;ItemTemplate&lt;/code&gt;, 只不过 &lt;code&gt;ListBox&lt;/code&gt; 需要用户注意这些内容:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;ListBox&lt;/code&gt; 的元素是可选中的, 如果你不希望元素可被选中, 那么使用 &lt;code&gt;ItemsControl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ListBox&lt;/code&gt; 是自带滚动条的, 如果你希望自己控制滚动条, 你可以使用 &lt;code&gt;ScrollViewer.XXX&lt;/code&gt; 附加属性来对它操作&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;ListBox&lt;/code&gt; 的元素是选中的, 这就意味着, 这个东西只适合一些数据的 &quot;选择&quot;, 例如我要填一个表单, 然后给用户选择一个数据.  如果我要做一个聊天窗口, 聊天窗口里面的聊天消息, 显然是一个 &quot;容器&quot;, 然后每一个消息气泡, 都是一个元素, 这个完全可以由绑定解决, 而使用 ListBox 会使消息可选中, 这不好, 我只是想让他自动生成控件, 所以使用 ItemsControl.&lt;/p&gt;
&lt;p&gt;不过, 如果你要用 &lt;code&gt;ListBox&lt;/code&gt; 做一个 &lt;em&gt;&quot;导航栏&quot;&lt;/em&gt; 的话, 确实是一个不错的选择, 因为可以直接订阅 &lt;code&gt;SelectionChanged&lt;/code&gt;, 并在事件中处理页面跳转的逻辑.&lt;/p&gt;
</content:encoded></item><item><title>[Win32] 在不创建窗口的情况下接收处理消息.</title><link>https://slimenull.com/posts/20230328075624/</link><guid isPermaLink="true">https://slimenull.com/posts/20230328075624/</guid><description>不创建常规窗口, 而是创建 &quot;仅消息&quot; 窗口用来处理窗体消息</description><pubDate>Tue, 28 Mar 2023 07:56:24 GMT</pubDate><content:encoded>&lt;p&gt;如果你在使用一个控制台程序, 并且还希望处理一些全局快捷键, 那么这个方法是合适的.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里说的不创建窗口指的是不创建可见的窗口&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;使用 MessageOnly 窗口&lt;/h2&gt;
&lt;p&gt;Windows 中存在一个特殊的窗体句柄 HWND_MESSAGE, 它的值是 -3, 当一个窗口的父窗口是它的时候, 这个窗口会成为 &quot;仅消息窗口&quot;&lt;/p&gt;
&lt;p&gt;使用仅消息窗口可以发送和接收消息。 它不可见、没有 z 顺序、无法枚举且不接收广播消息。 该窗口只是调度消息。&lt;/p&gt;
&lt;p&gt;若要创建仅消息窗口，请在 &lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-createwindowexa&quot;&gt;CreateWindowEx&lt;/a&gt; 函数的 hWndParent 参数中指定HWND_MESSAGE常量或现有仅消息窗口的句柄。 还可以通过在 SetParent 函数的 hWndNewParent 参数中指定HWND_MESSAGE，将现有窗口更改为仅消息窗口。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;参考: &lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/winmsg/window-features#message-only-windows&quot;&gt;窗口功能:仅消息窗口 -Win32 apps | Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;WPF 的使用示例:&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 窗口创建的参数
HwndSourceParameters hwndSourceParameters =
    new HwndSourceParameters()
    {
        HwndSourceHook = Hook,
        ParentWindow = (IntPtr)(-3),    // a magic window handle
    };

// 创建窗口
hwndSource =
    new HwndSource(hwndSourceParameters);

// Hook 消息方法定义
IntPtr Hook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
    // 这里处理消息
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[Web] 简单瀑布流布局实现</title><link>https://slimenull.com/posts/20230214094547/</link><guid isPermaLink="true">https://slimenull.com/posts/20230214094547/</guid><description>使用少量 JS 和 CSS 实现的瀑布流布局</description><pubDate>Tue, 14 Feb 2023 09:45:47 GMT</pubDate><content:encoded>&lt;p&gt;目前的纯 CSS 布局, 是没办法实现比较完美的瀑布流布局的.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;参考: &lt;a href=&quot;https://blog.csdn.net/qq_53008257/article/details/124638563&quot;&gt;CSS总结:瀑布流布局 - 黑白程序员
&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我使用 JS + CSS, 并且自动布局实现了较为简单, 观赏性好的瀑布流布局.&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;css/style.css&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;button onclick=&quot;add_new()&quot;&amp;gt;add&amp;lt;/button&amp;gt;
    &amp;lt;button onclick=&quot;start_loop()&quot;&amp;gt;start loop&amp;lt;/button&amp;gt;
    &amp;lt;button onclick=&quot;stop_loop()&quot;&amp;gt;stop loop&amp;lt;/button&amp;gt;
    
    &amp;lt;!--瀑布流容器--&amp;gt;
    &amp;lt;div id=&quot;masonry&quot;&amp;gt;
        &amp;lt;!--瀑布流列--&amp;gt;
        &amp;lt;div class=&quot;masonry-column-container&quot;&amp;gt;
            &amp;lt;div class=&quot;masonry-column&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;masonry-column-container&quot;&amp;gt;
            &amp;lt;div class=&quot;masonry-column&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;masonry-column-container&quot;&amp;gt;
            &amp;lt;div class=&quot;masonry-column&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;masonry-column-container&quot;&amp;gt;
            &amp;lt;div class=&quot;masonry-column&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;masonry-column-container&quot;&amp;gt;
            &amp;lt;div class=&quot;masonry-column&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;masonry-column-container&quot;&amp;gt;
            &amp;lt;div class=&quot;masonry-column&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;masonry-column-container&quot;&amp;gt;
            &amp;lt;div class=&quot;masonry-column&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;script&amp;gt;
        // 包装一个简单的随机数
        function random(start, end) {
            return Math.random() * (end - start) + start;
        }
        
        // 在瀑布流中添加一个元素
        function add_new() {
            let masonry = document.getElementById(&quot;masonry&quot;);
            let columns = masonry.querySelectorAll(&quot;.masonry-column&quot;);
            let minHeightColumn = columns[0];

            // 拿到高度最低的列
            columns.forEach(ele =&amp;gt; {
                if (ele.scrollHeight &amp;lt; minHeightColumn.scrollHeight) {
                    minHeightColumn = ele;
                }
            });

            // 创建一个新元素 (设置高度, 背景颜色)
            let new_item = document.createElement(&apos;div&apos;);
            new_item.classList.add(&apos;item&apos;);
            new_item.style.height = `${Math.random() * 200 + 70}px`;
            new_item.style.backgroundColor = `rgb(${random(0, 255)},${random(0, 255)},${random(0, 255)})`

            // 在高度最低的列中添加元素
            minHeightColumn.appendChild(new_item);
        }

        var masonry_loop;
        function start_loop() {
            masonry_loop = setInterval(add_new, 50);
        }

        function stop_loop() {
            clearInterval(masonry_loop);
        }
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CSS:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 瀑布流容器 */
#masonry {
    margin: 0 auto;
    width: 80vw;       /* 居中 */
    display: grid;     /* 网格布局 */
    grid-template-columns: repeat(7, 1fr);   /* 总共 7 列 */
    gap: 10px;                               /* 间距 10px */
}

/* 指定列是相对位置 (其中的元素可以相对列来定位) */
#masonry .masonry-column {
    position: relative;
}

/* 限制内容的宽度占满列, 加上边距, 圆角 */
#masonry .item {
    width: 100%;
    margin-top: 10px;
    border-radius: 1em;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;在 CodePen 上查看: &lt;a href=&quot;https://codepen.io/slimenull-the-bashful/pen/YzOzKmV&quot;&gt;Simple Masonry&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>[笔记] 关于通过鼠标滚轮设置缩放的技巧</title><link>https://slimenull.com/posts/20230131213623/</link><guid isPermaLink="true">https://slimenull.com/posts/20230131213623/</guid><description>鼠标滚轮缩放的简单算法</description><pubDate>Tue, 31 Jan 2023 21:36:23 GMT</pubDate><content:encoded>&lt;p&gt;鼠标正向滚动的时候, 拿到的滚动数据是大于 0 的, 反之, 小于 0.&lt;/p&gt;
&lt;p&gt;Pow 函数在 x 为正数的时候, 返回值是大于 1 的, x 为负数的时候, 返回值是小于 1 的, 可以直接设置这样的一个逻辑:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;缩放系数 = 缩放系数 * Pow(10, 鼠标滚动数据 / 减少滚动速度);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就能做一个简单的鼠标缩放了&lt;/p&gt;
</content:encoded></item><item><title>[.NET] 结构体布局详解与结构体内存对齐具体方式</title><link>https://slimenull.com/posts/20230111031653/</link><guid isPermaLink="true">https://slimenull.com/posts/20230111031653/</guid><description>.NET 结构体有三种布局方式, 序列布局, 显式布局, 自动布局, 其中序列布局存在 &quot;内存对齐&quot; 现象, 内存对齐有两种情况, 一个是字段自己的要求, 一个是当字段大小小于包大小时, 使用包作为对齐要求调整偏移量. 如果不希望进行内存对齐, 指定 Pack 为 1 即可.自动布局的结构体不应该进行与非托管的互操作, 因为成员顺序会改变</description><pubDate>Wed, 11 Jan 2023 03:16:53 GMT</pubDate><content:encoded>&lt;h2&gt;内存布局&lt;/h2&gt;
&lt;p&gt;一般的, 内存布局我们是不需要关心的, 因为我们直接通过字段或属性来访问结构体, 但是与非托管库操作的时候, 有时候就需要注意结构体布局了, 只有保证布局一致, 才能保证直接传结构体指针时, 非托管代码能正常访问到成员.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[StructLayout(LayoutKind.Sequential)]    // 声明 StructLayout 
struct MyStruct
{ }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;序列布局 (Sequential)&lt;/h3&gt;
&lt;p&gt;顺序布局就是按照你在结构体中声明成员的顺序, 一个个将它们放到内存中, 不过需要注意的是, 这些成员不是一个个紧挨着的, 他们可能存在内存对齐, 不过这个我们下面会详细讲到.&lt;/p&gt;
&lt;h3&gt;显式布局 (Explicit)&lt;/h3&gt;
&lt;p&gt;在这种布局中, 你需要指定结构体中每一个字段在这个结构体中的偏移量, 例如你有一个结构体, 它里面有两个 int, 你希望这两个 int 之间隔开 2 字节的大小, 那么只需要为第一个结构体指定偏移量为 0, 第二个结构体偏移量为 6 即可.&lt;/p&gt;
&lt;h3&gt;自动布局 (Auto)&lt;/h3&gt;
&lt;p&gt;在这种布局中, 你不应该进行与非托管的互操作, 因为为了性能, 结构体中的成员顺序会被自动调整. 例如下面这个明显没办法在不调整顺序与不添加间隔的情况下做到内存对齐的结构体, 它的成员顺序, 会被调整.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[StructLayout(LayoutKind.Auto)]
struct SomeIntegers
{
    byte AByte;
    short AShortInteger;
    byte AnotherByte;

    // 你实际得到的可能是 byte, byte, short 这样的一个结构体
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;内存对齐&lt;/h2&gt;
&lt;p&gt;当你使用序列布局的时候, 结构体成员会有内存对齐现象, 而在进行内存对齐时, 会有以下行为:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一个成员的内存偏移量, 应该能够被它自身所占大小整除&lt;/li&gt;
&lt;li&gt;如果一个成员占用内存大于包(Pack)的大小, 那么不再要求它的偏移量能被它自身大小整除, 而是能够被包大小整除即可.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;什么是包&lt;/h3&gt;
&lt;p&gt;包就是内存对齐的要求大小, 例如在 Windows 中默认是 8 字节对齐, 像是一些大于八字节的数据, 按照 8 字节在内存中进行对齐即可.&lt;/p&gt;
&lt;h3&gt;偏移量要求&lt;/h3&gt;
&lt;p&gt;举个例子, 如果我们有一个 int(32位), 那么它的内存偏移量应该是 4, 8, 12 等这些能够被 4 整除的值, 同理, long(64位) 的偏移量也应该是 8, 16, 32 这些.&lt;/p&gt;
&lt;p&gt;举个例子, 下面这个结构体中, 成员 B 为了实现偏移量为 2, 在成员 A 后产生了 1 字节的空隙.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[StructLayout(LayoutKind.Sequential)]
struct SomeIntegers
{
    byte A;     // 1 byte
                // 1 byte
    short B;    // 2 bytes
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;成员占用大于包&lt;/h3&gt;
&lt;p&gt;举个例子, 在使用 8 字节的包大小时, 且在一个包内, 已经被使用了 4 字节, 如果你要装下一个 long(8字节), 那么显然这个包已经装不下这个字段了, 那么这个字段会放到下一个包.&lt;/p&gt;
&lt;p&gt;举个例子, 下面这个结构体中, 成员 B 为了做到 8 字节的对齐, 它与第一个成员之间, 产生了 4 字节的空隙.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[StructLayout(LayoutKind.Sequential)]
struct SomeIntegers
{
    int A;    // 4 bytes
              // 4 bytes
    long B;   // 8 bytes
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是当你指定 Pack 为 4 时, 这个 long 则不再要求偏移量能被 8 整除, 而是被 4 整除即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct SomeIntegers
{
    int A;    // 4 bytes
    long B;   // 8 bytes (B 与 A 之间的空隙没有了)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此, 当你不希望这个结构体产生任何空隙, 或者不希望这个结构体有内存对齐时, 指定 Pack = 1 就可以解决问题. 因为这样会导致所有字段的偏移量能被 1 整除即可, 于是他们对于偏移量, 就没有了任何要求.&lt;/p&gt;
&lt;h3&gt;成员尾部留空&lt;/h3&gt;
&lt;p&gt;一个结构体尾部也会产生一些空余的, 不被使用的字节, 这个字节大小取决于&lt;strong&gt;结构体中最大的成员大小&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;例如我一个结构体中, 有一个 &lt;code&gt;long&lt;/code&gt;, 有一个 &lt;code&gt;byte&lt;/code&gt;, 最大成员大小为 8, 所以结构体的大小一定是 8 的倍数.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[StructLayout(LayoutKind.Sequential)]
struct TwoIntegers  // 大小共计 16 bytes
{
    long A;  // 8 bytes
    byte B;  // 1 byte
             // 7 bytes
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;当结构体嵌套&lt;/h3&gt;
&lt;p&gt;例如我一个结构体中包含另外一个结构体, 那么此时, 内存如何对齐呢?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;结构体字段与前一个字段之间会产生的间隙, 取决于结构体中最大的对齐大小.&lt;/li&gt;
&lt;li&gt;结构体字段自身所存在的尾部留空内存, 仍然会在外层结构体中保留&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;1. 结构体字段前的间隙&lt;/h4&gt;
&lt;p&gt;例如一个结构体中, 有一个 &lt;code&gt;int&lt;/code&gt; 字段以及一个 &lt;code&gt;byte&lt;/code&gt; 字段, 它的最大对其大小是 4, 也就是说, 这个结构体在作为其他结构体的成员时, 也会使用 4 作为对齐大小.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[StructLayout(LayoutKind.Sequential)]
struct SomeIntegers
{
    public byte A;              // 1 byte
                                // 3 bytes (结构体最大对齐是 4, 所以这里留出了 4 - 1 = 3 个字节)
    public TwoIntegers B;       // 8 bytes
    public byte C;              // 1 byte
                                // 3 bytes
}

[StructLayout(LayoutKind.Sequential)]
struct TwoIntegers
{
    int A;     // 4 bytes
    byte B;    // 1 
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 结构体尾部留空&lt;/h4&gt;
&lt;p&gt;即便结构体成员尾部的留空能够装下下一个成员, 它也不会这样做. &quot;结构体自己的内存空间完整不可侵犯&quot;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[StructLayout(LayoutKind.Sequential)]
struct SomeIntegers
{
    public byte A;              // 1 byte
                                // 3 byte
    public TwoIntegers B;       // 8 bytes
    public byte C;              // 1 byte   (尽管上一个结构体字段后有留空, 但这段留空不会被重复利用)
                                // 3 bytes  (所有成员的最大大小是 4, soyi这里留
}

[StructLayout(LayoutKind.Sequential)]
struct TwoIntegers
{
    int A;    // 4 bytes
    byte B;   // 1 byte
              // 3 bytes
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实现联合体&lt;/h2&gt;
&lt;p&gt;C++ 中有联合体这个东西, 实现多个字段共用一些数据, 在 C# 中, 如果你要实现这个, 使用显式布局即可.&lt;/p&gt;
&lt;p&gt;举个例子, 在下面这个 C++ 定义的结构体中, 存在两个字段 A 和 B, 他们共用相同的内存区域.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct SomeIntegers
{
    union {
        int A;
        int B;
    };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 C# 中实现这个, 你可以使用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[StructLayout(LayoutKind.Explicit)]
struct SomeIntegers
{
    [FieldOffset(0)]
    int A;

    [FieldOffset(0)]
    int B;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者这样的 C++ 结构体:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct SomeIntegers
{
    union {
        int A;
        struct {
            short Head;
            short Tail;
        };
    };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以这样用 C# 进行编写:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[StructLayout(LayoutKind.Explicit)]
struct SomeIntegers
{
    [FieldOffset(0)]
    int A;               // 占 4 字节

    [FieldOffset(0)]
    short Head;          // 占 2 字节

    [FieldOffset(2)]
    short Tail;          // 占 2 字节
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[.NET] 平台调用(P/Invoke) 与 DllImport 使用的相关讲解与注意事项,</title><link>https://slimenull.com/posts/20230106035930/</link><guid isPermaLink="true">https://slimenull.com/posts/20230106035930/</guid><description>P/Invoke 实现, DllImport 使用及成员变量概述, 字符串传值方式, 托管字符串对于不同字符集函数字符串的封送方式, 指针参数的正确声明姿势, DllImport 搜找入口点的大概逻辑</description><pubDate>Fri, 06 Jan 2023 03:59:30 GMT</pubDate><content:encoded>&lt;p&gt;通过对静态外部方法标记 &lt;code&gt;DllImport&lt;/code&gt; 特性, 我们可以实现使用 C# 调用非托管动态链接库的函数, 这种使用方式就叫做平台调用(Platform Invoke, 或 P/Invoke)&lt;/p&gt;
&lt;h2&gt;基本使用:&lt;/h2&gt;
&lt;p&gt;下面, 我们通过一个获取控制台窗口句柄的函数来演示最基本的平台调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System.Runtime.InteropServices;

[DllImport(&quot;kernel32.dll&quot;)]                # DllImport 特性与函数所在链接库
static extern IntPtr GetConsoleWindow();   # 方法基本声明 (静态外部方法)

IntPtr currentConsoleWindow = GetConsoleWindow();
Console.WriteLine($&quot;当前控制台的窗口句柄是: 0x{currentConsoleWindow:X}&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;DllImport 特性成员字段&lt;/h2&gt;
&lt;h4&gt;1. EntryPoint (入口点)&lt;/h4&gt;
&lt;p&gt;指示要调用的 DLL 入口点的名称或序号。&lt;/p&gt;
&lt;p&gt;可以指定入口点函数名称, 也可以按入口点序号标识入口点. 序号的前缀为 # 符号，例如 #1。 如果省略此字段, CLR 会使用标记了当前 &lt;code&gt;DllImport&lt;/code&gt; 特性的方法名称作为入口点名称&lt;/p&gt;
&lt;h4&gt;2. CharSet (字符集)&lt;/h4&gt;
&lt;p&gt;指示方法调用时要使用的字符集, CLR 会将传入的字符串参数按照指定字符集封送到对应函数.&lt;/p&gt;
&lt;p&gt;将此字段与枚举的成员 &lt;code&gt;CharSet&lt;/code&gt; 一起使用, 可以指定字符串参数的封送处理行为, 并指定要调用的入口点名称 (给定的确切名称或以“A”或“W”为后缀的名称). C# 和 Visual Basic 的默认枚举成员是 &lt;code&gt;CharSet.Ansi&lt;/code&gt; , C++ 的默认枚举成员是 &lt;code&gt;CharSet.None&lt;/code&gt;, 这等效于 &lt;code&gt;CharSet.Ansi&lt;/code&gt;. 在 Visual Basic 中, 使用 &lt;code&gt;Declare&lt;/code&gt; 语句指定 &lt;code&gt;CharSet&lt;/code&gt; 字段.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;举个例子, 当你入口点名称为 GetWindowText 并且 CharSet 为 Unicode 时, 实际调用的方法是 GetWindowTextW.
(因为虽然不存在 GetWindowText 函数, 但 CLR 搜索到了 GetWindowTextW 函数, 所以就会调用它)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;3. SetLastError (设置最后一个错误)&lt;/h4&gt;
&lt;p&gt;指定调用方在返回之前是否设置一个错误(在 Windows 上调用 &lt;code&gt;SetLastError&lt;/code&gt; 或者其他平台上调用 &lt;code&gt;errno&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;如果这个字段设置为 &lt;code&gt;true&lt;/code&gt;, 运行时的&lt;a href=&quot;#ex1&quot;&gt;封送拆收器^1^&lt;/a&gt;会调用 &lt;code&gt;GetLastError&lt;/code&gt; 或 &lt;code&gt;errno&lt;/code&gt; 并缓存返回值以避免它被其他的 API 调用覆盖. 你可以在 .NET 6.0 及以上版本中调用 &lt;code&gt;GetLastPInvokeError&lt;/code&gt; 或在 .NET 5 及以下版本与 .NET Framework 中调用 &lt;code&gt;GetLastWin32Error&lt;/code&gt; 来接收错误码.&lt;/p&gt;
&lt;p&gt;在 .NET 中, 当这个字段设置为 &lt;code&gt;true&lt;/code&gt; 时, 错误信息在调用被调用方之前会被清除(设置为0). 但在 .NET Framework 中, 错误信息则不会被清除. 这意味着由 &lt;code&gt;GetLastPInvokeError&lt;/code&gt; 和 &lt;code&gt;GetLastWin32Error&lt;/code&gt; 返回的错误信息, 在 .NET 中只表示上一个拥有 &lt;code&gt;DllImportAttribute.SetLastError&lt;/code&gt; 值设定为 &lt;code&gt;true&lt;/code&gt; 的 &lt;code&gt;DlllImport&lt;/code&gt; 特性的平台调用的错误信息. 而在 .NET Framework 中, 这个错误信息可以由一个平台调用保留到下一个.&lt;/p&gt;
&lt;h4&gt;4. ExtractSpelling (精准拼写)&lt;/h4&gt;
&lt;p&gt;控制 CLR 是否根据 CharSet 字段的值来在非托管 DLL 中搜索入口点名称, 或直接使用指定的入口点名称.&lt;/p&gt;
&lt;p&gt;当值为 false, 且在找不到指定入口点名称的函数时, CLR 会根据 &lt;code&gt;CharSet&lt;/code&gt; 字段的值来搜索入口点名称. 此时, 当 &lt;code&gt;CharSet&lt;/code&gt; 为 &lt;code&gt;CharSet.Ansi&lt;/code&gt; 时, 会尝试调用尾部追加字母 &apos;A&apos; 的入口点名称, 当 &lt;code&gt;CharSet&lt;/code&gt; 为 &lt;code&gt;CharSet.Unicode&lt;/code&gt; 时, 则会尝试调用尾部追加字母 &apos;W&apos; 的入口点名称. 通常, 托管编译器会设定这个字段的值.&lt;/p&gt;
&lt;p&gt;在 VB 以及 C# 和 C++ 中, ExactSpelling 的值会根据 CharSet 的值有不同的表现.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;语言&lt;/th&gt;
&lt;th&gt;ANSI&lt;/th&gt;
&lt;th&gt;Unicode&lt;/th&gt;
&lt;th&gt;Auto&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Visual Basic&lt;/td&gt;
&lt;td&gt;ExactSpelling := True&lt;/td&gt;
&lt;td&gt;ExactSpelling := True&lt;/td&gt;
&lt;td&gt;ExactSpelling := False&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C#&lt;/td&gt;
&lt;td&gt;ExactSpelling = false&lt;/td&gt;
&lt;td&gt;ExactSpelling = false&lt;/td&gt;
&lt;td&gt;ExactSpelling = false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C++&lt;/td&gt;
&lt;td&gt;ExactSpelling = false&lt;/td&gt;
&lt;td&gt;ExactSpelling = false&lt;/td&gt;
&lt;td&gt;ExactSpelling = false&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;也就是说, 在 VB 中, 如果你将 CharSet 设置为确切的字符集, ANSI 或者 Unicode 时, ExactSpelling 会是 True, CLR 不会再去搜索与字符集匹配的名称, 而是直接使用你匹配的名称. 而在 C# 和 C++ 中, 即便你指定了确切的字符集, CLR 也会在找不到你指定的入口点名称时, 尝试搜索与字符集相匹配的函数.&lt;/p&gt;
&lt;h4&gt;5. CallingConvertion (调用约定)&lt;/h4&gt;
&lt;p&gt;指定入口点的&lt;a href=&quot;#ex2&quot;&gt;调用约定^2^&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;你可以将它设置为 &lt;code&gt;CallingConvertion&lt;/code&gt; 枚举的一个成员. 这个 &lt;code&gt;CallingConvertion&lt;/code&gt; 字段的默认值是 &lt;code&gt;Winapi&lt;/code&gt;, 它默认在 Windows 平台上是 &lt;code&gt;StdCall&lt;/code&gt; 约定, 并且其他所有平台上是 &lt;code&gt;Cdecl&lt;/code&gt; 约定.&lt;/p&gt;
&lt;h4&gt;6. BestFitMapping (最合适映射)&lt;/h4&gt;
&lt;p&gt;将 Unicode 字符转换为 ANSI 字符时, 启用或禁用最佳映射行为.&lt;/p&gt;
&lt;h4&gt;7. PreserveSig&lt;/h4&gt;
&lt;p&gt;指示是否直接转换具有 &lt;code&gt;HRESULT&lt;/code&gt; 返回值的非托管方法，或者 &lt;code&gt;HRESULT&lt;/code&gt; 是否自动将返回值转换为异常.&lt;/p&gt;
&lt;h4&gt;8. ThrowOnUnmappableChar&lt;/h4&gt;
&lt;p&gt;启用或禁用在遇到已被转换为 ANSI &quot;?&quot; 字符的无法映射的 Unicode 字符时引发异常.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;对于字符串的处理&lt;/h2&gt;
&lt;h4&gt;1. 传入字符串, 但不更改.&lt;/h4&gt;
&lt;p&gt;例如我的动态链接库中有以下函数:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;windows.h&amp;gt;
extern &quot;C&quot; __declspec(dllexport) void PrintW(wchar_t* lpstr)
{
    wprintf(L&quot;%s\n&quot;, lpstr);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它接收一个宽字符串指针, 并将它打印到标准输出流中. 下面列举常用的字符串传值方式&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;直接传 string&lt;pre&gt;&lt;code&gt;[DllImport(&quot;test.dll&quot;, EntryPoint = &quot;PrintW&quot;, CharSet = CharSet.Unicode)]
extern static void Print1(string str);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;传 StringBuilder&lt;pre&gt;&lt;code&gt;[DllImport(&quot;test.dll&quot;, EntryPoint = &quot;PrintW&quot;, CharSet = CharSet.Unicode)]
extern static void Print1(StringBuilder str);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;传 char 数组&lt;pre&gt;&lt;code&gt;[DllImport(&quot;test.dll&quot;, EntryPoint = &quot;PrintW&quot;, CharSet = CharSet.Unicode)]
extern static void Print1(char[] str);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;传 char 指针&lt;pre&gt;&lt;code&gt;[DllImport(&quot;test.dll&quot;, EntryPoint = &quot;PrintW&quot;, CharSet = CharSet.Unicode)]
extern static void Print1(char* str);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2. 传入字符串并做更改&lt;/h4&gt;
&lt;p&gt;注意, 如果在函数中&lt;strong&gt;有对字符串更改&lt;/strong&gt;, 那么, 你需要注意, 传入的 &lt;code&gt;string&lt;/code&gt; 可能会被更改. 例如我们有以下函数:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define _CRT_SECURE_NO_WARNINGS
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;windows.h&amp;gt;

extern &quot;C&quot; __declspec(dllexport) void FuckYouWorldW(wchar_t* lpstr)
{
    wsprintf(lpstr, L&quot;Fuck you world, 撒比世界&quot;);
}

extern &quot;C&quot; __declspec(dllexport) void FuckYouWorldA(char* lpstr)
{
    sprintf(lpstr, &quot;Fuck you world, 撒比世界&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;当使用 Unicode 所对应的函数时, string 会被更改:&lt;pre&gt;&lt;code&gt;[DllImport(&quot;test.dll&quot;, EntryPoint = &quot;FuckYouWorldW&quot;,CharSet = CharSet.Unicode)]
extern static void FuckYouWorldW(string str);

string buf = new string(&apos;\0&apos;, 32);   // 声明一个长度为 32 的字符串
FuckYouWorldW(buf);

Console.WriteLine(buf);              // 你会得到一个 &quot;Fuck you world, 撒比世界&quot;
                                     // 但是注意, buf 字符串后面还是有很多 \0 的, 只是没打印出来
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;但是当你使用 ANSI 所对应的函数时, 传入的 string 即便被更改了, 也不会体现出来:&lt;pre&gt;&lt;code&gt;[DllImport(&quot;test.dll&quot;, EntryPoint = &quot;FuckYouWorldA&quot;,CharSet = CharSet.Ansi)]
extern static void FuckYouWorldA(string str);

string buf = new string(&apos;\0&apos;, 32);   // 声明一个长度为 32 的字符串

// 在传入 buf 时, Marshaler 会帮我们将字符串转为 ANSI 字符串, 并传入指针
FuckYouWorldA(buf);
// 这导致, 虽然函数变更了指针指向的字符串值, 但没有对我们原来的字符串有任何更改

Console.WriteLine(buf);              // 你什么也看不到
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;如果你使用 &lt;code&gt;StringBuilder&lt;/code&gt; 的时候, 无论是 Unicode 还是 ANSI 方法, 你都能看到函数对字符串的更改, Marshal 会帮助我们处理 Unicode 字符串与 ANSI 字符串的转换, 并且处理字符串变更后的结果, 所以你无论如何都能看到变更后的字符串.&lt;/li&gt;
&lt;li&gt;如果你使用 &lt;code&gt;char[]&lt;/code&gt;, 它的表现和 &lt;code&gt;string&lt;/code&gt; 一样, 能否看到结果取决于你使用的字符集&lt;/li&gt;
&lt;li&gt;如果你使用 &lt;code&gt;char*&lt;/code&gt;, 虽然原始的指针会直接传进去, 但是如果你在使用 ANSI 的函数, 那么肯定的, 你得到的更改后的字符串并不会正常显示. 因为它是 ANSI 格式的, 而 C# 按照 Unicode 来解码字符串.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;参考: &lt;a href=&quot;https://blog.csdn.net/EricChina/article/details/5265991&quot;&gt;Marshaling between Managed and Unmanaged Code&lt;/a&gt;
原文似乎找不到了, 只有 CSDN 的转载&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;3. 我应该使用哪种方式传字符串?&lt;/h4&gt;
&lt;p&gt;当你需要向函数传入字符串, 并且这个字符串不会被函数更改时, 我建议你直接传入 &lt;code&gt;string&lt;/code&gt;, 这样最方便. 而当你需要传出字符串时, 也就是调用类似于 &lt;code&gt;GetWindowTextW&lt;/code&gt; 这类函数时, 我建议你使用 &lt;code&gt;StringBuilder&lt;/code&gt;, 提前设置好容量, 并在调用函数时, 传入这个容量. 这样函数就能够正确的处理, 并且不造成有关内存访问的异常.&lt;/p&gt;
&lt;p&gt;当然, 传入其他的类型也完全可以, 这取决于你的需求, 例如你大量操作非托管的类型, 你的代码周围全都是一些指针, 那么你大可以将函数声明为指针, 然后直接调用.&lt;/p&gt;
&lt;h4&gt;4. 关于字符串拷贝&lt;/h4&gt;
&lt;p&gt;有人认为, 在调用非托管动态链接库的时候, CLR 会拷贝一份 &lt;code&gt;string&lt;/code&gt; 供使用, 这并不完全正确.&lt;/p&gt;
&lt;p&gt;如果你在使用 Unicode 版本的函数(设置了 &lt;code&gt;CharSet&lt;/code&gt; 为 Unicode), 那么在调用的时候, 完全不会有任何拷贝, 而是直接使用源字符串, 这也就是为什么这种情况下函数可以对字符串进行更改并正确得到结果&lt;/p&gt;
&lt;p&gt;如果你在使用 ANSI 版本的函数(设置了 &lt;code&gt;CharSet&lt;/code&gt;), 那么在调用的时候, CLR 会帮你把 .NET 字符串转为非托管的 ANSI 字符串, 并传入指针, 显然这个过程造成了字符串拷贝.&lt;/p&gt;
&lt;h4&gt;5. 我应该使用哪个字符集的函数?&lt;/h4&gt;
&lt;p&gt;至少在 Windows 平台中, 你最好使用 Unicode 字符集的函数, 也就是后缀带 W 的函数. 因为在高版本的 Windows 平台内部也是使用的宽字符串(Wide char string), 而且 C# 的 string 可以直接传入, 而不经任何操作, 这样性能较好.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;参考: &lt;a href=&quot;https://stackoverflow.com/questions/16926002/wide-string-vs-string-does-it-affect-performance-in-windows-c&quot;&gt;Wide String vs String , Does it affect performance in Windows C++&lt;/a&gt;
这里表明, Windows 内核使用的是宽字符&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;注意:&lt;/h4&gt;
&lt;p&gt;不要尝试直接取字符串地址并传入, 因为你取的地址不能直接作为 &lt;code&gt;wchar_t*&lt;/code&gt; 使用, 而是栈上这个变量的地址, 你需要进行一次 &lt;code&gt;指针取值&lt;/code&gt; 操作才能拿到栈上存储的字符串地址值, 并且 &lt;code&gt;string&lt;/code&gt; 在堆上的存储内容还包含一个 &quot;类型头&quot;, 你至少需要计算这个偏移量才可以. 总而言之, 很复杂, 不要这么做.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;对于指针的处理&lt;/h2&gt;
&lt;p&gt;如果一个动态链接库的函数传入一个 &lt;code&gt;int&lt;/code&gt; 指针, 并且它将会更改这个指针指向的 &lt;code&gt;int&lt;/code&gt; 作为返回值, 你可以直接使用 C# 的 &lt;code&gt;out&lt;/code&gt; 关键字来声明这个参数:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extern &quot;C&quot; __declspec(dllexport) void add(int a, int b, int* result)
{
    *result = a + b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[DllImport(&quot;test.dll&quot;, EntryPoint = &quot;add&quot;)]
extern static void add(int a, int b, out int result);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同理, 如果一个函数传入一个 &lt;code&gt;int&lt;/code&gt; 指针, 它会读取这个指针指向的 &lt;code&gt;int&lt;/code&gt; 值, 也会更改指针指向的值, 那么你可以使用 C# 的 &lt;code&gt;ref&lt;/code&gt; 关键字来声明这个参数:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extern &quot;C&quot; __declspec(dllexport) void add114514(int* val)
{
    *val += 114514;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[DllImport(&quot;test.dll&quot;, EntryPoint = &quot;add&quot;)]
extern static void add114514(ref int val);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是注意, 你不能使用 &lt;code&gt;out string&lt;/code&gt;, 这个雀食是不可以的, 想要传出一个字符串, 你得用 &lt;code&gt;StringBuilder&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;对于结构体的处理&lt;/h2&gt;
&lt;p&gt;事实上, 在进行非托管动态链接库函数调用的时候, CLR 会将所有我们用到的值都转换为非托管的格式, 然后进行封送. 例如在 C# 中, &lt;code&gt;bool&lt;/code&gt; 占一个字节, 但是在 WinAPI 中, &lt;code&gt;bool&lt;/code&gt; 可是要占四个字节的. 调用参数的传入与返回值的返回, 都有 CLR 参与这个转换.&lt;/p&gt;
&lt;p&gt;例如我们有这样的一个 C++ 函数:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct StringWrapper
{
    wchar_t* StrPtr;
    int Length;
};

extern &quot;C&quot; __declspec(dllexport) void print(StringWrapper* str)
{
    for (int i = 0; i &amp;lt; str-&amp;gt;Length; i++) {
        putwchar(str-&amp;gt;StrPtr[i]);
    }

    putwchar(L&apos;\n&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么我在编写 C# 的时候, 就可以这样写:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]   // 注意, 这里需要写明字符集
struct StringWrapper
{
    string _value;       // 字符串会自动被转换为正确的 wchar_t 指针以供使用
    int    _length;      // 整数的话, 则是原封不动的传过去

    public StringWrapper()
    {
        _value = string.Empty;
        _length = 0;
    }

    public StringWrapper(string value)
    {
        _value = value;
        _length = value.Length;
    }
}

[DllImport(&quot;test.dll&quot;, EntryPoint = &quot;print&quot;, CharSet = CharSet.Unicode)]
private extern static void PrintString([In, Optional] ref StringWrapper str);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是注意, 当参数是结构体指针, 并且你希望这个结构体能够被 CLR 进行封送, 那么你必须在 &lt;code&gt;DllImport&lt;/code&gt; 那里, 将方法参数声明为 &lt;code&gt;ref&lt;/code&gt; 或 &lt;code&gt;out&lt;/code&gt; 参数, 而不是直接声明为结构体的指针.&lt;/p&gt;
&lt;p&gt;还有就是, 非常重要的一点, 当结构体进行封送的时候, 里面的字符串会根据结构体的 &lt;code&gt;StructLayout&lt;/code&gt; 所指定的 &lt;code&gt;CharSet&lt;/code&gt; 进行封送, 默认是 ANSI, 所以会被封送为 &lt;code&gt;char*&lt;/code&gt;, 如果你要使用 &lt;code&gt;wchar_t*&lt;/code&gt;, 那你必须指定 &lt;code&gt;CharSet&lt;/code&gt; 为 &lt;code&gt;CharSet.Unicode&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你希望你的结构体能够在直接传指针的情况下仍然可用, 那么你必须使你的结构体内存布局与非托管中结构体的内存布局保持一致.
参考: &lt;a href=&quot;/posts/20230111031653/&quot;&gt;[.NET] 结构体布局详解与结构体内存对齐具体方式&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;入口点查找大概逻辑&lt;/h2&gt;
&lt;p&gt;下面是伪代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;如果存在 Xxx 函数:
    调用 Xxx 函数;
    返回;
否则:
    如果 ExactSpelling:   // 精确拼写
        抛异常(&quot;入口点找不到&quot;);
    否则:
        如果 CharSet 是 Auto:
            如果是高系统版本:
                确认 CharSet 为 Unicode;
            否则:
                确认 CharSet 为 ANSI;
                
        如果 CharSet 是 Unicode:
            如果存在 XxxW 函数:   // 就是后面加了个 W 后缀
                调用 XxxW 函数;
                返回;
            否则:
                抛异常(&quot;入口点找不到&quot;);
        否则如果 CharSet 是 ANSI:
            如果存在 XxxA 函数:   // 就是后面加了个 A 后缀
                调用 XxxA 函数;
                返回;
            否则:
                抛异常(&quot;入口点找不到&quot;);
        否则:
            抛异常(&quot;入口点找不到&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;注解&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&amp;lt;span id=&quot;ex1&quot;&amp;gt;封送拆收器:&amp;lt;/span&amp;gt; CLR 在与非托管动态链接库操作时, 负责将托管类型转换为非托管或将非托管类型转换为托管类型.&lt;/li&gt;
&lt;li&gt;&amp;lt;span id=&quot;ex2&quot;&amp;gt;调用约定:&amp;lt;/span&amp;gt; 调用约定（Calling Convention）是规定子函数(过程)如何获取参数以及如何返回的方案. 其通常与架构, 编译器等相关.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;文章会不定时更新, 以 CSDN 这边内容为准&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>[C++] 栈上存储以及堆中存储, 值引用, 类型拷贝以及拷贝构造函数, 将类分配到堆中</title><link>https://slimenull.com/posts/20221115125040/</link><guid isPermaLink="true">https://slimenull.com/posts/20221115125040/</guid><description>[C++] 栈上存储以及堆中存储, 值引用, 类型拷贝以及拷贝构造函数, 将类分配到堆中</description><pubDate>Tue, 15 Nov 2022 12:50:40 GMT</pubDate><content:encoded>&lt;p&gt;C++ 中的 class, 简直和 C#/Java 中的完全不一样了, 因为 C++ 的 class... 是分配在栈上的.&lt;/p&gt;
&lt;h2&gt;赋值时的拷贝行为&lt;/h2&gt;
&lt;p&gt;一个单纯的 class, 其实也和 int 一样, 在被赋值时, 会被拷贝, 你可以理解为, 他和结构体其实没啥区别, 只是 class 能够声明方法罢了.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

class something
{
    public:
    int integer;
};

int main()
{
    something sth1 = something();                // 新建一个实例
    sth1.integer = 123;                          // 改 integer 的值

    something sth2 = sth1;                       // sth2 将是一个新的实例, 是 sth1 的拷贝
    sth2.integer = 114514;                       // 改 integer 的值

    std::cout &amp;lt;&amp;lt; sth1.integer &amp;lt;&amp;lt; std::endl;      // 输出 123
    std::cout &amp;lt;&amp;lt; sth2.integer &amp;lt;&amp;lt; std::endl;      // 输出 114514
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;标准库中的类都会尝试拷贝所有内容&lt;/h2&gt;
&lt;p&gt;当你赋值一个 std::vector 的时候, 它也会拷贝自身, 然而它是包含非栈上的内容的(用动态内存存储值), 尽管如此, 它也会拷贝所有的东西, 包括容器中的元素.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;

int main()
{
    std::vector&amp;lt;int&amp;gt; vec1 = std::vector&amp;lt;int&amp;gt;{ 1, 2, 3 };
    std::vector&amp;lt;int&amp;gt; vec2 = vec1;                         // vec2 是 vec1 的拷贝

    vec2.push_back(114514);

    std::cout &amp;lt;&amp;lt; vec1.size() &amp;lt;&amp;lt; std::endl;                // 输出 3
    std::cout &amp;lt;&amp;lt; vec2.size() &amp;lt;&amp;lt; std::endl;                // 输出 4
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;通过 &amp;amp; 来使用引用&lt;/h2&gt;
&lt;p&gt;如果你不希望在变量赋值时进行拷贝, 可以使用 &amp;amp; 对变量进行修饰, 基本的数据类型, 诸如 int, char 是可以的, 同样, class 和 struct 也一样&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;可以通过指针的概念来帮助理解 &quot;引用&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main()
{
    int integer = 123;
    int&amp;amp; ref_integer = integer;

    ref_integer = 114514;

    std::cout &amp;lt;&amp;lt; integer &amp;lt;&amp;lt; std::endl;         // 输出 114514
    std::cout &amp;lt;&amp;lt; ref_integer &amp;lt;&amp;lt; std::endl;     // 输出 114514
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类默认只会进行浅拷贝&lt;/h2&gt;
&lt;p&gt;例如我们自定义一个类, 类中声明一个指针, 在类进行拷贝时, 指针会被拷贝, 然而新的指针还是指向了原来的值.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;

class something
{
    public:
    int* p_integer;     // 一个指针成员
};

int main()
{
    int integer = 123;                           // 先定义一个整数

    something sth1 = something();                // 定义一个 sth1
    sth1.p_integer = &amp;amp;integer;                   // 改它的 p_integer

    something sth2 = sth1;                       // 获取它的拷贝
    *sth2.p_integer = 114514;                    // 对 sth2 的指针目标值进行更改

    std::cout &amp;lt;&amp;lt; *sth1.p_integer &amp;lt;&amp;lt; std::endl;     // 输出 114514
    std::cout &amp;lt;&amp;lt; *sth2.p_integer &amp;lt;&amp;lt; std::endl;     // 输出 114514
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;通过自己声明拷贝构造函数来实现深拷贝&lt;/h2&gt;
&lt;p&gt;如果要实现对某些指针成员的深拷贝, 就需要自己定义一个拷贝构造函数了, 只需要声明一个参数为相同类型引用的构造函数即可. (你也可以添加 const 修饰)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;

class something
{
    public:
    int* p_integer;
    something();                             // 无参构造函数
    something(const something&amp;amp; origin);      // 拷贝构造函数 (传入相同类型的引用
};

int main()
{
    int integer = 123;

    something sth1 = something();
    sth1.p_integer = &amp;amp;integer;

    something sth2 = sth1;
    *sth2.p_integer = 114514;

    std::cout &amp;lt;&amp;lt; *sth1.p_integer &amp;lt;&amp;lt; std::endl;      // 输出 123
    std::cout &amp;lt;&amp;lt; *sth2.p_integer &amp;lt;&amp;lt; std::endl;      // 输出 114514
}

something::something()
{
    p_integer = NULL;
}

something::something(const something&amp;amp; origin)      // 拷贝构造
{
    p_integer = NULL;
    if (origin.p_integer != NULL)
    {
        p_integer = (int*)malloc(sizeof(int));     // 在堆中分配一个 int

        if (p_integer != NULL)                     // 如果内存分配成功
        {
            *p_integer = *origin.p_integer;        // 把指针目标值拷贝了
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类中包含另一个类成员, 成员也会被正确拷贝&lt;/h2&gt;
&lt;p&gt;例如我们定义了一个类, 然后这个类中用了 std::vector 成员, 在拷贝时, 这个成员也会被 C++ 默认的拷贝构造来进行拷贝&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;

class something
{
    public:
    std::vector&amp;lt;int&amp;gt; vec;
};

int main()
{
    something sth1 = something();
    sth1.vec = std::vector&amp;lt;int&amp;gt;{ 1, 2, 3 };      // 设置 sth1 的 vec 为 3 个元素

    something sth2 = sth1;                       // 拷贝一份 sth1
    sth2.vec.push_back(114514);                  // 往 sth2 的 vec 中添加一个新元素

    std::cout &amp;lt;&amp;lt; sth1.vec.size() &amp;lt;&amp;lt; std::endl;   // 输出 3
    std::cout &amp;lt;&amp;lt; sth2.vec.size() &amp;lt;&amp;lt; std::endl;   // 输出 4
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;要千万小心野指针问题&lt;/h2&gt;
&lt;p&gt;因为类的实例是直接分配在栈上的, 所以千万不能直接返回它的指针, 否则,,, 就野指针了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;

std::vector&amp;lt;int&amp;gt;* new_vector()
{
    std::vector&amp;lt;int&amp;gt; vec = std::vector&amp;lt;int&amp;gt;{ 1, 2, 3 };    // 这个 vector 会在栈上
    return &amp;amp;vec;                                           // 返回它的指针
}

int main()
{
    std::vector&amp;lt;int&amp;gt;* p_vec = new_vector();                // 你虽然拿到了指针, 但是栈已经弹出了

    std::cout &amp;lt;&amp;lt; p_vec-&amp;gt;size() &amp;lt;&amp;lt; std::endl;               // 输出 0 (也有可能是其他的值
}                                                          // 总之不会输出正确的值
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;通过 new 来将类分配到堆中&lt;/h2&gt;
&lt;p&gt;为了避免上面的野指针问题, 我们需要把类型实例存储到堆中, 而不是栈中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;

std::vector&amp;lt;int&amp;gt;* new_vector()
{
    std::vector&amp;lt;int&amp;gt;* vec = new std::vector&amp;lt;int&amp;gt;{ 1, 2, 3 };   // 使用 new 将实例存储到堆中
    return vec;                                                // 返回它
}

int main()
{
    std::vector&amp;lt;int&amp;gt;* p_vec = new_vector();

    std::cout &amp;lt;&amp;lt; p_vec-&amp;gt;size() &amp;lt;&amp;lt; std::endl;            // 输出 3
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然, 你也可以配合 &quot;引用&quot; 来食用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;

std::vector&amp;lt;int&amp;gt;&amp;amp; new_vector()
{
    std::vector&amp;lt;int&amp;gt;* vec = new std::vector&amp;lt;int&amp;gt;{ 1, 2, 3 };
    return *vec;
}

int main()
{
    std::vector&amp;lt;int&amp;gt;&amp;amp; p_vec = new_vector();            // 注意, 这里必须写 std::vector&amp;lt;int&amp;gt;&amp;amp;

    std::cout &amp;lt;&amp;lt; p_vec.size() &amp;lt;&amp;lt; std::endl;            // 输出 3
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C++] C++ 的常量究竟是什么? 它与 C# 和 Java 中的常量有什么区别? 应该如何理解常量?</title><link>https://slimenull.com/posts/20221115110633/</link><guid isPermaLink="true">https://slimenull.com/posts/20221115110633/</guid><description>更好的认识 C++ 中的 const</description><pubDate>Tue, 15 Nov 2022 11:06:33 GMT</pubDate><content:encoded>&lt;p&gt;在 C++ 中, const 虽然是常量, 然而这个常量比起 C#/Java 中的常量, 似乎又有点不对劲&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;文章面向的人群是拥有 C#/Java 基础的程序员&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;运行时赋值&lt;/h2&gt;
&lt;p&gt;C++ 的常量不是在编译时就直接确定值的, 你在运行时也可以初始化它, 下面是示例代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main()
{
    int input;
    std::cin &amp;gt;&amp;gt; input;

    const int unchangable_integer = input;
    std::cout &amp;lt;&amp;lt; unchangable_integer &amp;lt;&amp;lt; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;不可直接变更&lt;/h2&gt;
&lt;p&gt;const 变量一旦初始化之后, 就不再可以直接更改 (直接对其赋值)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main()
{
    int input;
    std::cin &amp;gt;&amp;gt; input;

    const int unchangable_integer = input;
    
    unchangable_integer = 114514;           // 这里编译错误
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;强制更改常量&lt;/h2&gt;
&lt;p&gt;如果一个常量是在运行时确认值的, 那么可以通过指针强制更改, 并且在二次取值的时候也能够拿到更改后的值.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main()
{
    int input;
    std::cout &amp;lt;&amp;lt; &quot;:&quot;;
    std::cin &amp;gt;&amp;gt; input;                                     // 输入 123

    const int unchangable_integer = input;

    std::cout &amp;lt;&amp;lt; unchangable_integer &amp;lt;&amp;lt; std::endl;         // 输出 123

    int* changable_integer = (int*)&amp;amp;unchangable_integer;   // 转为指针
    *changable_integer = 114514;                           // 赋值
    
    std::cout &amp;lt;&amp;lt; unchangable_integer &amp;lt;&amp;lt; std::endl;         // 输出 114514 (值被成功更改了)
    std::cout &amp;lt;&amp;lt; *changable_integer &amp;lt;&amp;lt; std::endl;          // 输出 114514
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果一个常量是在编译时就确认值了, 虽然也可以更改, 但是却有些奇怪表现...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main()
{
    const int integer = 123;
    int* mutable_integer = (int*)&amp;amp;integer;        // 取可更改的指针

    *mutable_integer = 114514;                    // 改值

    const int* p_integer = &amp;amp;integer;

    std::cout &amp;lt;&amp;lt; integer &amp;lt;&amp;lt; std::endl;               // 直接输出, 结果是 123
    std::cout &amp;lt;&amp;lt; *mutable_integer &amp;lt;&amp;lt; std::endl;      // 输出指针目标值, 结果是 114514

    std::cout &amp;lt;&amp;lt; *&amp;amp;integer &amp;lt;&amp;lt; std::endl;             // 取地址然后输出目标值, 结果是 13
    std::cout &amp;lt;&amp;lt; *p_integer &amp;lt;&amp;lt; std::endl;            // 取保存为变量的地址的目标值, 结果是 114514
    
    std::cout &amp;lt;&amp;lt; &amp;amp;integer &amp;lt;&amp;lt; std::endl;              // 取地址, 结果是 0073FDE0
    std::cout &amp;lt;&amp;lt; mutable_integer &amp;lt;&amp;lt; std::endl;       // 打印之前的指针, 结果是 0073FDE0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于上面这些奇怪的表现, 笔者认为, 在编译时直接确认值的常量, 对其的直接引用会被编译器优化为常量, 而不会去取存储了的常量值.&lt;/p&gt;
&lt;p&gt;甚至, 如果这个常量压根就没有取地址操作, 那么在程序编译后压根就不会有存储这个变量的, 栈上的内存空间, 它直接就是字面量, 存储在程序 rdata 段(只读数据段), 完全无法变更.&lt;/p&gt;
&lt;h2&gt;我不会对它进行赋值&lt;/h2&gt;
&lt;p&gt;如果你在一个函数中声明一个参数, 而这个参数是 const, 那在这里, const 表示的是, 你不打算对这个变量进行赋值(更改).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

void print_int(const int number)
{
    printf(&quot;%d\n&quot;, number);
}

int main()
{
    const int integer = 123;
    int mutable_integer = 123;

    print_int(integer);            // 可以传入只读整数
    print_int(mutable_integer);    // 也可以传入普通整数
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;标记了参数为 const 的参数, 就表示函数不会对他进行变更, 这时调用方也能放心的使用它.&lt;/p&gt;
&lt;p&gt;所以, 对于我们平常所编写的函数, 如果不会对参数进行变更, 也可以加上 const 修饰符来使函数的功能更加清晰可读.&lt;/p&gt;
&lt;h1&gt;结语&lt;/h1&gt;
&lt;p&gt;希望这些简短的介绍能够帮助你更好的认识 C++, 毕竟 C#/Java 和 C++ 的差别还是非常非常大的&lt;/p&gt;
&lt;p&gt;另外, 如果上述内容有错误, 欢迎在评论区指出&lt;/p&gt;
</content:encoded></item><item><title>[C#, 笔记] 启用虚拟终端处理 (使用 ANSI 转义序列前需启用)</title><link>https://slimenull.com/posts/20221107060011/</link><guid isPermaLink="true">https://slimenull.com/posts/20221107060011/</guid><description>无法使用 ANSI 转义序列, 无法通过 \e \1b 逃逸字符打印彩色或格式化内容, 通过调用 WinAPI 启用虚拟终端处理来解决问题</description><pubDate>Mon, 07 Nov 2022 06:00:11 GMT</pubDate><content:encoded>&lt;p&gt;不启用虚拟终端会导致:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;无法使用 ANSI 转义序列, 无法通过 \e \1b 逃逸字符打印彩色或格式化内容&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通过以下代码来在 Windows 上启用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;internal class PlatformUtils
{
    private const int STD_OUTPUT_HANDLE = -11;
    private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
    private const uint DISABLE_NEWLINE_AUTO_RETURN = 0x0008;

    [DllImport(&quot;kernel32.dll&quot;)]
    private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);

    [DllImport(&quot;kernel32.dll&quot;)]
    private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);

    [DllImport(&quot;kernel32.dll&quot;, SetLastError = true)]
    private static extern IntPtr GetStdHandle(int nStdHandle);

    [DllImport(&quot;kernel32.dll&quot;)]
    private static extern uint GetLastError();

    public static uint EnableVirtualTerminalProcessingOnWindows()
    {
        var iStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
        if (!GetConsoleMode(iStdOut, out uint outConsoleMode))
        {
            return GetLastError();
        }

        outConsoleMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
        if (!SetConsoleMode(iStdOut, outConsoleMode))
        {
            return GetLastError();
        }

        return 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在运行前加上以下代码即可:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
    PlatformUtils.EnableVirtualTerminalProcessingOnWindows();
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C#,笔记] NAudio 傅里叶变换的 forward 参数与 m 参数</title><link>https://slimenull.com/posts/20221025074833/</link><guid isPermaLink="true">https://slimenull.com/posts/20221025074833/</guid><description>NAudio FourierTransform.FFT 的参数值</description><pubDate>Tue, 25 Oct 2022 07:48:33 GMT</pubDate><content:encoded>&lt;p&gt;forward 是是否对结果取平均值 (除以样本数), 这样样本数量就不会影响结果值的大小.
m 是要进行变换的数量, 但是是 2 的指数, 也就是说 2^m 必须小于等于样本数
因为傅里叶变换只能进行 2^m 个数据的傅里叶变换&lt;/p&gt;
</content:encoded></item><item><title>[全解] 刷机, BL 锁, Bootloader, Recovery, Magisk, Root, ADB, 线刷, 卡刷, 9008, 绕过 FRP</title><link>https://slimenull.com/posts/20221022104302/</link><guid isPermaLink="true">https://slimenull.com/posts/20221022104302/</guid><description>刷机, BL 锁, Bootloader, Recovery, Magisk, Root, ADB, 线刷, 卡刷, 9008, 绕过 FRP 全解</description><pubDate>Sat, 22 Oct 2022 10:43:02 GMT</pubDate><content:encoded>&lt;p&gt;这篇文章讲解手机刷机, Root 的教程, 以及过程中可能遇到的大多数问题.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;你可能需要对电脑重装系统有些了解, 知道分区, 设备, 驱动是什么东西, 并且能够熟悉使用电脑, 以及在命令窗口中执行命令&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;手机刷机, 当然也可以说成重装系统. 一般我们使用电脑重装系统的时候, 都是通过 PE 或者写入了安装镜像的存储介质, 引导, 然后安装, 非常简单. 但是手机刷机的过程与电脑不一样.&lt;/p&gt;
&lt;p&gt;电脑之所以能够随便重装系统, 那是因为电脑中的设备, 都有着相同的通讯协议, 以及电脑有着完善的驱动, 可以随便使用. 例如我插入了一个电脑无法识别的设备, 那么只需要从官网下载其对应的驱动, 那么电脑就可以使用这个设备了.&lt;/p&gt;
&lt;p&gt;而且电脑中很多设备, 都是通用的协议, 例如存储设备, 摄像机, 音频设备之类的, 这些设备很多都不需要为其安装专用驱动即可直接使用.&lt;/p&gt;
&lt;h3&gt;1. 分区&lt;/h3&gt;
&lt;p&gt;对于电脑来讲, 用户可以随意分区, 但是手机的分区都是预先分好的, 使用者在使用的过程中, 一般是不会对其进行重新分区, 而且手机中的分区也都指定好了作用.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;手机分区包含: modem, bootloader, boot, recovery, system, data, cache&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;bootloader:&lt;/strong&gt;
其中, bootloader 分区, 如其名, 用来引导启动, 可以理解为类似于电脑中 MBR 或者 GRUB 的存在, 其中存放着用来引导启动的程序.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;recovery:&lt;/strong&gt;
recovery 分区则用来进行系统的一些高级的维护, 例如恢复出厂设置什么的. 一般的, 手机默认携带的 recovery 分区内的程序, 不包含高级功能, 但是我们可以通过刷入第三方的 recovery, 实现更多功能, 这也是刷机的主要步骤.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;boot:&lt;/strong&gt;
boot 分区包含系统的内核 (Linux)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;system:&lt;/strong&gt;
系统分区, 包含了系统本体, 除了内核以外的所有部分&lt;/p&gt;
&lt;h3&gt;2. 高权限&lt;/h3&gt;
&lt;p&gt;对于电脑来讲, 通过外部设备引导进入 PE, 于是就能够随心所欲的对物理机中的其他设备进行任何操作, 例如重新分区, 安装系统什么的, 但是安卓不能这样.&lt;/p&gt;
&lt;p&gt;对于安卓来讲, 首先我们要进入 bootloader, 在开机时长按 &lt;code&gt;音量-&lt;/code&gt; 和 &lt;code&gt;电源&lt;/code&gt; 键, 即可进入 bootloader, 此时通过数据线将电脑与手机链接, 然后电脑就可以通过 &quot;fastboot&quot; 工具对手机进行操作&lt;/p&gt;
&lt;p&gt;但是手机一般都会对 bootloader 进行限制, 使你无法进行危险操作, 例如刷入第三方 recovery 之类的, 所以在使用 fastboot 之前, 你还需要将 BL 锁给解除.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;目前一加, 小米这两个厂商开放性比较强, 官方提供了解除 BL 锁的方案, OPPO 和 VIVO 则封闭, 基本上没办法刷机与 ROOT (除了旧手机)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;解除 BL 锁&lt;/h2&gt;
&lt;p&gt;对于开放性最强的一加, 解除 BL 锁没有任何难度, 但是不同型号有不同的解除方式, 有的只需要执行指令, 有的则需要进入开发者选项打开 OEM 解锁.&lt;/p&gt;
&lt;p&gt;对于 小米/红米 手机, 则需要使用官方的解锁工具进行解锁, 并且还需要将手机与小米账户绑定, 然后等待一个星期什么的, 总之挺麻烦的.&lt;/p&gt;
&lt;p&gt;OPPO / vivio 的话, 就完全不用想刷机了,,, 很难的啦, 哦对了, 华为也一样&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在使用官方提供的解 BL 锁工具时, 设备可能识别不到, 你需要先安装设备对应的驱动才可以, 而对应工具中一般都提供了这个功能.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;网上也有很多强制解 BL 锁的奇妙方法, 甚至连 OPPO 强解 BL 锁的都有, 但是不适用于所有设备, 你也可以尝试, 但是解了 BL 锁, 最多弄个 Root 权限, 刷第三方系统就算了, 因为他们不适配 OPPO / vivo 的设备&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;准备工作&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;下载安卓平台工具 (ADB 和 FastBoot):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;官网: &lt;a href=&quot;https://developer.android.google.cn/studio/releases/platform-tools&quot;&gt;https://developer.android.google.cn/studio/releases/platform-tools&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在里面下载适用于你电脑的工具, 然后解压即可. 下载到的内容至少应该包含 adb.exe 和 fastboot.exe&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;下载你所需要的刷机包:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;常用的刷机包都是卡刷包(zip格式), 它可以通过 TWRP 直接进行安装, 也可以在电脑上通过 ADB 旁加载的方式进行安装.&lt;/p&gt;
&lt;p&gt;下面介绍的通过 ADB 测加载的方式安装卡刷包的方式.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;卡刷包的名称由来... 早期都是把刷机包存到存储卡里, 然后装系统的时候, 直接在 TWRP 中选择存储卡中的刷机包, 就可以安装, 除了刷入 TWRP 的过程, 其他过程都不需要电脑进行操作.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;线刷包是直接通过 FastBoot 就能直接安装的刷机包, 不需要刷入 TWRP, 但是第三方系统是不会做这种刷机包的, 一般都是官方提供线刷包, 用来救砖(线刷包是基本不会失败的刷机方式)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;刷入 TWRP&lt;/h2&gt;
&lt;p&gt;TWRP 全称 Team Win Recovery Project, 是一个开放源码软件的定制Recovery映像, 供基于安卓的设备使用. 它提供了一个支持触摸屏的界面, 有很多奇妙的小功能, 例如擦除手机数据, 安装系统, 备份系统, 通过旁加载让电脑 ADB 刷入系统什么的. 总而言之, 刷机必用. (Root 也是)&lt;/p&gt;
&lt;p&gt;官网: &lt;a href=&quot;https://twrp.me/&quot;&gt;https://twrp.me/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;你需要在 TWRP 的官网下载与自己设备对应的 TWRP 镜像 (img文件)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;手机进入 BootLoader (关机状态下, 长按 &lt;code&gt;音量-&lt;/code&gt; 和 &lt;code&gt;电源&lt;/code&gt; )&lt;/li&gt;
&lt;li&gt;连接手机与电脑&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;现在在电脑上使用命令行, 执行 &lt;code&gt;fastboot devices&lt;/code&gt; 查看连接是否正常, 你的控制台应该输出类似于以下内容的信息:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hyxobieieaqgcinr        fastboot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;左侧时手机名称, 右侧是手机当前所处于的状态.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;现在执行 &lt;code&gt;fastboot flash recovery twrp.img&lt;/code&gt; 指令, 其中 &lt;code&gt;twrp.img&lt;/code&gt; 应该是你下载下来的 TWRP 镜像文件路径.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果成功刷入, 你的控制台应该输出类似于以下内容的信息:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Sending &apos;recovery&apos; (65536 KB)                      OKAY [  1.670s]
Writing &apos;recovery&apos;                                 OKAY [  0.538s]
Finished. Total time: 2.228s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完上述步骤之后, TWRP 就成功刷入到你的手机中了, 但是注意, TWRP 是可能会被你的手机系统重新覆盖掉, 如果此时你重启手机, 进入手机系统, 你的 TWRP 可能会丢的 &lt;s&gt;(MIUI 就喜欢干这事儿)&lt;/s&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;执行 &lt;code&gt;fastboot reboot recovery&lt;/code&gt; 以使手机直接重启到 recovery 中&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果成功进入, 你的屏幕上会显示 TEAMWIN 的字样 (样子怪怪的, 刚开始我是没认出来的)&lt;/p&gt;
&lt;h2&gt;清除手机数据&lt;/h2&gt;
&lt;p&gt;网上有着很多, 例如双清, 三清, 四清啥的, 其实就是清除几个分区的数据罢了. 具体安装系统的时候还是要以该系统安装的官方教程为准, 例如 Pixel Experience 只需要清除 Data 分区的数据即可&lt;/p&gt;
&lt;p&gt;在 TWRP 主页面选择 清除(Wipe) 然后里面有默认的三清以及右侧的格式化 Data.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;网上对于双清的说法也不同, 有的人说清除 Data + Cache, 有的人说清除 Cache + Dalvik, 笔者也不清楚具体怎么称呼, 总而言之, 按需清除. 大多数装系统都是清除 Data + Cache + Dalvik&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果要清除更多数据, 例如将 System 分区和 Internal Storage 分区清除, 使用左边的高级按钮就完事儿了.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我是个憨批,,, 这文章写着写着, 不小心就操作上了, 然后我手机数据就被我手贱, 清除掉了! (这么惨了, 还不点个赞...)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;侧加载刷机包&lt;/h2&gt;
&lt;p&gt;现在, 在电脑打开命令行, 执行 &lt;code&gt;adb devices&lt;/code&gt;, 如果设备连接正常, 则会显示以下内容:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List of devices attached
hyxobieieaqgcinr        recovery
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在手机的 TWRP 主页面中, 点击左下角的 高级(Advanced), 再点击 ADB Sideload, 然后滑动滑块, 开始 Sideload&lt;/p&gt;
&lt;p&gt;然后在电脑上执行 &lt;code&gt;adb sideload xxx.zip&lt;/code&gt;, 其中 &lt;code&gt;xxx.zip&lt;/code&gt; 是你下载的卡刷包.&lt;/p&gt;
&lt;p&gt;等待完成, 然后重启手机即可 (手机上有 重启系统(Reboot System) 按钮, 也可以在电脑上执行 &lt;code&gt;adb reboot&lt;/code&gt; 重启手机&lt;/p&gt;
&lt;p&gt;重启完毕后, 如果一切操作没有问题, twrp 和刷机包都是与手机适配的, 那么你现在大抵是已经进入了系统了.&lt;/p&gt;
&lt;h2&gt;Magisk 与 Root&lt;/h2&gt;
&lt;p&gt;Magisk（也被称作面具）是一套开放源代码的Android（5.0以上版本）自定义工具套组，内置了Magisk Manager（图形化管理界面）、Root、启动脚本、SElinux补丁和启动时认证/dm-verity/强制加密移除功能.&lt;/p&gt;
&lt;p&gt;简而言之, 这玩意儿可以用来拿 Root 权限, 而且操作步骤极其简单.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;下载 Magisk 的安装包 (是一个 apk 文件)&lt;/li&gt;
&lt;li&gt;将 apk 文件复制出来一份, 后缀改为 zip&lt;/li&gt;
&lt;li&gt;手机进入 Recovery, 并准备 sideload&lt;/li&gt;
&lt;li&gt;电脑执行 &lt;code&gt;adb sideload magisk.zip&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;没错, Magisk 的刷入, 和刷入系统一样, 都是通过侧加载进行的. (注意, 不需要清除数据)&lt;/p&gt;
&lt;p&gt;操作完毕并重启后, 你的手机上就会多一个图标很奇怪的 Magisk 软件, 点击它, 它会提示你 &quot;安装完整 Magisk&quot; 啥的, 下一步就是直接在手机上安装 Magisk 这个软件了, 你可以直接拿刚刚的安装包用来安装, 当然更简单的方式是:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;电脑执行 &lt;code&gt;adb install magisk.apk&lt;/code&gt;, 然后手机就直接装上 Magisk 软件了.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在以上步骤完成之后, 你的手机就成功 Root 了, 别的软件可以请求 Root 权限, 而你需要的只是在 Magisk 内同意或者拒绝即可.&lt;/p&gt;
&lt;h2&gt;9008&lt;/h2&gt;
&lt;p&gt;这东西是仅骁龙处理器支持的刷机方式, 通过短接主板上的指定触点或者通过奇妙的深度刷机线短接两根线来进入对应模式&lt;/p&gt;
&lt;p&gt;因为我还没逝过这个方式(我手机红米 Note 8 Pro, 是联发科的处理器), 所以这里就不讲了, 网上已经有完善的教程了.&lt;/p&gt;
&lt;h2&gt;绕过 FRP&lt;/h2&gt;
&lt;p&gt;如果你刷入的是国际版的手机系统, 并且它需要谷歌验证, 例如 Pixel Experience 这种类原生系统, 你可能会遇到无法验证的问题. 毕竟国内无法正常访问谷歌的服务.&lt;/p&gt;
&lt;p&gt;这个问题是因为在之前的旧系统中, 有登陆谷歌账户, 并且在刷机前没有清除这个数据. 下面给出参考的解决方案:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;手机连接电脑 WiFi 并使用电脑上的代理. (科学上网)&lt;/li&gt;
&lt;li&gt;直接清空 FRP 数据, 只需要使用 adb, 执行 &lt;code&gt;adb shell&lt;/code&gt; 进入手机命令行, 执行以下指令:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;dd if=/dev/zero of=/dev/block/bootdevice/by-name/frp
dd if=/dev/zero of=/dev/block/bootdevice/by-name/config
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在谷歌账户管理中, 找到自己登陆过谷歌账户的设备(当前手机), 然后退出账户.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;以上方案任意一个都可以直接使用, 但是不保证所有手机都可以绕过. 例如我实测第三个方案是不能用的, 第二个方案可以直接使用, 但是有些别的用户反应, 在 Android 13 上需要使用方案三来解决.
方案一在我这里也没有通过测试, 但是理论上这个是完全可以使用的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;如果在执行 &lt;code&gt;dd if=/dev/zero of=/dev/block/bootdevice/by-name/frp&lt;/code&gt; 时执行失败, 可以加上额外的参数, 如下:
&lt;code&gt;dd if=/dev/zero of=/dev/block/bootdevice/by-name/frp bs=512 count=1024&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;flowchat
st=&amp;gt;start: 开始
e=&amp;gt;end: 结束
bl=&amp;gt;operation: 解 BL 锁
blok=&amp;gt;condition: 成功?
ready=&amp;gt;operation: 准备工作(下载所需 TWRP 和刷机包)
twrp=&amp;gt;operation: 刷入 TWRP
wipe=&amp;gt;operation: 清除手机数据
sideload=&amp;gt;operation: 通过侧加载刷入刷机包

hasbl=&amp;gt;condition: 手机有 BL 锁?
st-&amp;gt;hasbl
hasbl(yes)-&amp;gt;bl-&amp;gt;blok
hasbl(no)-&amp;gt;ready
blok(yes)-&amp;gt;ready
blok(no)-&amp;gt;bl

ready-&amp;gt;twrp-&amp;gt;wipe-&amp;gt;sideload-&amp;gt;e
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>如何关掉 PC QQ 烦人的 QQ 频道</title><link>https://slimenull.com/posts/20221017104608/</link><guid isPermaLink="true">https://slimenull.com/posts/20221017104608/</guid><description>通过禁用 QQGuild 文件夹的 &quot;执行&quot; 权限使 QQ 无法运行QQ 频道. 进而实现关闭 QQ 频道功能.</description><pubDate>Mon, 17 Oct 2022 10:46:08 GMT</pubDate><content:encoded>&lt;p&gt;QQ 频道被 QQ 强制捆绑, 很多人用不到, 而且还不能完全免打扰, 点击消息的时候, QQ 频道就会跳出来, 非常的烦人.&lt;/p&gt;
&lt;h2&gt;步骤&lt;/h2&gt;
&lt;p&gt;关闭掉 QQ. 然后找到 QQ 频道的本体目录:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用 %LOCALAPPDATA%\Tencent 可以快速跳转到这里&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/f3394c708be242149a4051a194a01fa2.png&quot; alt=&quot;直接跳转到QQ本体&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/e144607c0932435a98c3b9492d99c029.png&quot; alt=&quot;QQ频道本体目录&quot; /&gt;
打开文件夹属性, 转到 &quot;安全&quot; 选项卡, 然后点击 &quot;高级&quot;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/769fdd8b1eb742be942173bc62649504.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;禁用掉继承, 删掉所有用户的权限, 重新添加当前用户的权限, 点击展示高级权限, 然后指定以下权限(大部分都允许, 但是不允许执行文件):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/148804dbe31f499fa1784de54f7c4323.png&quot; alt=&quot;&quot; /&gt;
然后测试QQ频道本体打开时是否能够执行, 如果权限设置正确, 则会显示以下报错:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/8518be19918b4e8597f053efa52e7f2b.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注: 禁用继承是为了是 QQGuild 目录下直接继承 QQGuild 的权限, 因为默认 QQGuild 继承自上层的目录
然后为 QQGuild 设置它自己的权限, 于是 QQGuild 目录下的所有文件都继承了我们设置的权限&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最后, 启动 QQ, 你会发现, 烦人的 QQ 频道木有了~&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/fdbbbfc187c642e6b40484b8955f9e3d.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;另外&lt;/h2&gt;
&lt;p&gt;网上也有其他的解决方案, 就是直接删掉 QQGuild 目录, 但是截止到 2022/10/17 日, 已经不能使用. (视频链接 &lt;a href=&quot;https://www.bilibili.com/video/av683996745/&quot;&gt;https://www.bilibili.com/video/av683996745/&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;如果不给文件读取写入的权限, QQ 还会识别不出来这个程序, 然后又会 &quot;尝试下载安装&quot; 只不过文件已存在并且无法写入, 所以它最终不会做到什么操作, 但是每一次启动 QQ 都会下载, 占用网络, 烦人, 所以需要给 QQGuild 写入, 读取, 以及列举文件夹的权限.&lt;/p&gt;
</content:encoded></item><item><title>Chrome 拓展开发 Service Worker 无法使用 XMLHttpRequest 发送 HTTP 请求</title><link>https://slimenull.com/posts/20221015111704/</link><guid isPermaLink="true">https://slimenull.com/posts/20221015111704/</guid><description>Chrome 拓展开发 Servcie Worker 无法使用 XMLHttpRequest, 应该使用 fetch 替代</description><pubDate>Sat, 15 Oct 2022 11:17:04 GMT</pubDate><content:encoded>&lt;p&gt;相关的报错信息:
XMLHttpRequest is not defined at chrome-extension:&lt;/p&gt;
&lt;h2&gt;原因&lt;/h2&gt;
&lt;p&gt;显然, Service Worker 不支持 XMLHttpRequest&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;用 fetch&lt;/p&gt;
&lt;h2&gt;相关资料&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/8dc188509a7f4756bf88833ae8796085.png&quot; alt=&quot;&quot; /&gt;
通过 fetch 上传 JSON 数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = { username: &apos;example&apos; };    // 定义数据

fetch(&apos;https://example.com/profile&apos;, {
  method: &apos;POST&apos;, // or &apos;PUT&apos;            // 指定请求方法
  headers: {
    &apos;Content-Type&apos;: &apos;application/json&apos;,  // 指定内容类型
  },
  body: JSON.stringify(data),            // 传入内容
})
  .then((response) =&amp;gt; response.json())   // 取响应 json
  .then((data) =&amp;gt; {                      // 对解析完成的 json 进行操作
    console.log(&apos;Success:&apos;, data);
  })
  .catch((error) =&amp;gt; {                    // 捕捉错误
    console.error(&apos;Error:&apos;, error);
  });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者最简单的, 仅仅上传, 不考虑响应与异常情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = { username: &apos;example&apos; };    // 定义数据

fetch(&apos;https://example.com/profile&apos;, {
  method: &apos;POST&apos;, // or &apos;PUT&apos;            // 指定请求方法
  headers: {
    &apos;Content-Type&apos;: &apos;application/json&apos;,  // 指定内容类型
  },
  body: JSON.stringify(data),            // 传入内容
});
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Google Chrome 插件开发: 无法建立连接, 接收端不存在. Could not establish connection. Receiving end does not exist</title><link>https://slimenull.com/posts/20221014100019/</link><guid isPermaLink="true">https://slimenull.com/posts/20221014100019/</guid><description>Google Chrome 插件开发: 无法建立连接, 接收端不存在. Could not establish connection. Receiving end does not exist</description><pubDate>Fri, 14 Oct 2022 10:00:19 GMT</pubDate><content:encoded>&lt;h2&gt;通过以下代码向当前页面发送 &quot;start&quot; 消息:&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;chrome.tabs.query({active: true,currentWindow: true}, tabs =&amp;gt; {
    let tab = tabs[0];
    chrome.tabs.sendMessage(tab.id, &quot;start&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;报错:&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Uncaught (in promise) Error: Could not establish connection. Receiving end does not exist.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;可能的原因:&lt;/h2&gt;
&lt;p&gt;接收端, 也就是说目标页面必须有 chrome.runtime.onMessage 监听消息, 如果 &quot;content-script&quot; 没有注入到页面中, 那么这个页面就无法接收消息&lt;/p&gt;
&lt;p&gt;如果你的插件刚刚加载, 并且在一个已经加载完毕的页面中使用它, 则会出这个问题.&lt;/p&gt;
&lt;p&gt;因为这个页面已经加载完了, 它并没有被注入脚本, 你需要刷新页面, 使脚本注入到页面中, 然后才可以发送消息&lt;/p&gt;
&lt;h2&gt;平台不允许文章内容太少, 下面是水&lt;/h2&gt;
&lt;p&gt;sendMessage
chrome.tabs.sendMessage(integer tabId, any message, function responseCallback)
向指定标签页中的内容脚本发送一个消息，当发回响应时执行一个可选的回调函数。当前扩展程序在指定标签页中的每一个内容脚本都会收到 runtime.onMessage 事件。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;tabId&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;message&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;responseCallback&lt;/td&gt;
&lt;td&gt;optional function&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果您指定了 responseCallback 参数，它应该指定一个如下形式的函数：&lt;/p&gt;
&lt;p&gt;function(any response) {...};
response ( any )
请求处理程序发出的 JSON 响应对象。如果连接到指定标签页的过程中发生错误，将不传递参数调用回调函数，并将 runtime.lastError 设置为错误消息。&lt;/p&gt;
</content:encoded></item><item><title>泰拉瑞亚 游戏角色 Later version</title><link>https://slimenull.com/posts/20221012160055/</link><guid isPermaLink="true">https://slimenull.com/posts/20221012160055/</guid><description>泰拉瑞亚游戏角色显示 Later version. 是因为游戏角色在新版本的游戏中运行过, 所以旧版本会显示这个</description><pubDate>Wed, 12 Oct 2022 16:00:55 GMT</pubDate><content:encoded>&lt;p&gt;你的角色在版本更新的游戏中运行过, 旧版本无法打开新版本的角色. 你需要使用新版本游戏打开它&lt;/p&gt;
</content:encoded></item><item><title>[笔记] WPF TextBox GetRectFromCharacterIndex 返回 Width 为 0</title><link>https://slimenull.com/posts/20221010102217/</link><guid isPermaLink="true">https://slimenull.com/posts/20221010102217/</guid><description>WPF TextBox GetRectFromCharacterIndex 返回 Width 为 0, 获取指定字符的位置与宽高</description><pubDate>Mon, 10 Oct 2022 10:22:17 GMT</pubDate><content:encoded>&lt;p&gt;GetRectFromCharacterIndex(int index) 只返回字符左边缘的信息, 包含位置与高度&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//
// Summary:
//     返回指定索引处字符前边缘的矩形。
//
// Parameters:
//   charIndex:
//     要检索其矩形的字符的从零开始的字符索引。
//
// Returns:
//     字符前边缘处指定字符处的矩形 索引，或 System.Windows.Rect.Empty（如果无法确定边界矩形）
//     
//
public Rect GetRectFromCharacterIndex(int charIndex);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要获取某个 index 的字符的位置与大小, 可以通过 GetRectFromCharacterIndex 的重载获取字符右边缘的信息, 然后计算宽度&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//
// Summary:
//     返回字符前缘或后边缘的矩形 指定的索引。
//
// Parameters:
//   charIndex:
//     要检索其矩形的字符的从零开始的字符索引。
//
//   trailingEdge:
//     true 以获得字符的后边缘; false 以获得前边缘的角色。
//
// Returns:
//     指定字符索引处字符边边缘的矩形，或 System.Windows.Rect.Empty (如果无法确定边界矩形)
//
// Exceptions:
//   T:System.ArgumentOutOfRangeException:
//     字符索引为负数或大于内容的长度。
public Rect GetRectFromCharacterIndex(int charIndex, bool trailingEdge);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[Unity] 碰撞器, 触发器, 刚体,Dynamic, Kinematic, Static, OnCollision, OnTrigger 全讲</title><link>https://slimenull.com/posts/20220927145127/</link><guid isPermaLink="true">https://slimenull.com/posts/20220927145127/</guid><description>Unity 碰撞相关的几乎所有基础知识都在这里了, 高度整合以及精心标注!</description><pubDate>Tue, 27 Sep 2022 14:51:27 GMT</pubDate><content:encoded>&lt;p&gt;详细的讲解关于 Unity 中碰撞的各种细节, 文章以 Unity2D 为主讲起, 并且附上关于 Unity3D 的相关介绍&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;文章长期更新, 也欢迎评论区进行纠正或补充
另外, 如果你遇到了一些问题, 建议看完整篇文章, 在末尾有一些常见小问题的标注&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;碰撞器 / Collider&lt;/h2&gt;
&lt;p&gt;碰撞器是最基本的用来检测碰撞的玩意儿, 例如你要做物理效果, 需要一个墙, 一个球扔过去, 墙挡住这个球, 这个过程中就需要判断墙与球是否碰撞&lt;/p&gt;
&lt;p&gt;碰撞器的类型有很多, 例如 BoxCollider, CircleCollider 什么的, 但是用途都一样, 只不过检测的范围形状不一样罢了&lt;/p&gt;
&lt;h2&gt;触发器 / Trigger&lt;/h2&gt;
&lt;p&gt;碰撞器中有一个 IsTrigger 属性, 可以设定当前的碰撞器是否是 &quot;触发器&quot;, 如果设定了 true, 也就是当一个碰撞器是触发器时, 那么这个物体就不再参与任何物理碰撞.&lt;/p&gt;
&lt;p&gt;例如, 墙的碰撞器设置了 IsTrigger, 那么这个墙就不能再挡住一个球.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;虽然设置了触发器的游戏物体不参与物理碰撞, 但是它仍然有物理效果, 例如你可以设置他的速度, 重量, 什么的, 这些仍然是有效的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;刚体 / RigidBody&lt;/h2&gt;
&lt;p&gt;刚体是 Unity 中实现物理效果的最基本组件, 例如加了刚体的游戏对象可以受重力影响, 两个加了刚体的游戏对象再相撞时, 会像显示一样, &quot;阻拦&quot;, 而不是穿过彼此.&lt;/p&gt;
&lt;p&gt;刚体依赖于碰撞器, 当你添加刚体组件的时候, 他也会默认添加一个碰撞器, 如果你需要别的碰撞器, 可以自己手动更改&lt;/p&gt;
&lt;p&gt;刚体中有最为重要的一个选项 &quot;Body Type&quot;, 它的值可以是 动态的(Dynamic), 运动学的(Kinematic), 静态的(Static)&lt;/p&gt;
&lt;p&gt;在下面介绍各种类型的刚体时, &quot;是否产生碰撞&quot;表示是否会触发 &quot;OnCollision&quot; 系列的事件. &quot;受影响&quot; 标识该刚体是否会受目标刚体的力的影响.&lt;/p&gt;
&lt;p&gt;例如, Dynamic 会被 Kinematic 刚体阻拦, Dynamic 也可以被 Kinematic 撞飞, 但是 Kinematic 不会受 Dynamic 刚体的影响, 所以 Kinematic 不可能被 Dynamic 撞飞&lt;/p&gt;
&lt;p&gt;另外, 如果 A 刚体不会穿过 B 刚体, 那么说明 B 刚体对 A 产生影响, 也就是说, 当 A 刚体静止时, B 刚体以一定速度冲向 A, 那么可能&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Unity 3D 中没有 Static 刚体.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;动态的 / Dynamic&lt;/h2&gt;
&lt;p&gt;当一个刚体的 Body Type 是 Dynamic 时, 这个游戏对象拥有全部的物理效果, 包括重力, 游戏对象之间的作用力. 他会与其他的所有刚体有物理碰撞效果, 并且能够与其他所有触发 &quot;OnCollision&quot; 系列事件.&lt;/p&gt;
&lt;p&gt;在当前游戏对象为动态刚体时:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;另一对象刚体类型&lt;/th&gt;
&lt;th&gt;是否产生碰撞&lt;/th&gt;
&lt;th&gt;受影响&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;动态的刚体受一切刚体的影响, 具有所有物理特性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kinematic&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;在 Unity 3D 中,  一个刚体的默认行为就是 Dynamic&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;运动学的 / Kinematic&lt;/h2&gt;
&lt;p&gt;当一个刚体的 Body Type 是 Kinematic 时, 这个游戏对象仅拥有一部分物理效果, 并且消耗比动态刚体更少的性能.&lt;/p&gt;
&lt;p&gt;它不会受重力或其他任何力影响, 你可以理解为它拥有无限的质量, 其他任何游戏对象撞击它的时候, 都不会动摇他的位置.&lt;/p&gt;
&lt;p&gt;而且, 不会与其他 Kinematic 刚体产生物理碰撞, 因为 Kinematic 本身就不是为碰撞而设计的, 它更适合做墙或者需要用脚本手动操作的游戏对象, 当然也不会触发 &quot;OnCollision&quot; 系列事件.&lt;/p&gt;
&lt;p&gt;在当前游戏对象为运动学刚体时:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;另一对象刚体类型&lt;/th&gt;
&lt;th&gt;是否产生碰撞&lt;/th&gt;
&lt;th&gt;受影响&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;Kinematic 只会横冲直撞, 速度, 角动量不会受任何影响 (参考三体中的水滴)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kinematic&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;在 Unity 3D 中, 勾选 IsKinematic 选项来使当前刚体作为运动学刚体&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;静态的 / Static&lt;/h2&gt;
&lt;p&gt;静态的刚体不应该被移动, 或者说它的用途也是很少的, 因为它只会与 Kinematic 刚体产生碰撞. 在游戏中, 它应该作为墙, 或者地面.&lt;/p&gt;
&lt;p&gt;在当前游戏对象为静态刚体时:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;另一对象刚体类型&lt;/th&gt;
&lt;th&gt;是否产生碰撞&lt;/th&gt;
&lt;th&gt;受影响&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;你的小篮球还能把地球撞飞不成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kinematic&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;Static 刚体只能与 Dynamic 2D 刚体碰撞&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;因为这种刚体不是为了移动而设计的&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;碰撞事件 / OnCollision&lt;/h2&gt;
&lt;p&gt;两个刚体在碰撞的时候, 可以产生碰撞事件, 具体哪些对象可以产生碰撞事件, 在上面已经说了.&lt;/p&gt;
&lt;p&gt;需要注意的是, 一旦产生碰撞事件, 双方都会触发 OnCollision 的 Unity 消息. 而且所传入的 Collision 参数也是不一样的. 但是无论怎样, Collision 的 collider 属性都是与当前碰撞的碰撞体实例 (otherCollider 属性一般很少用到)&lt;/p&gt;
&lt;p&gt;碰撞事件有下面这些&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法名&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OnCollisionEnter / OnCollisionEnter2D&lt;/td&gt;
&lt;td&gt;当该碰撞体/刚体已开始接触另一个刚体/碰撞体时，调用 OnCollisionEnter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OnCollisionStay / OnCollisionStay2D&lt;/td&gt;
&lt;td&gt;对于接触另一个碰撞体或刚体的每个碰撞体或刚体，每帧调用一次 OnCollisionStay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OnCollisionExit / OnCollisionExit2D&lt;/td&gt;
&lt;td&gt;当该碰撞体/刚体已停止接触另一个刚体/碰撞体时，调用 OnCollisionExit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;触发事件 / OnTrigger&lt;/h2&gt;
&lt;p&gt;使用触发器, 并且要引发触发事件, 必须保证: 两个互相碰撞的物体均包含碰撞器, 并且至少有一个碰撞器设置了 IsTrigger 为 true, 至少其中一个必须包含非静态(Static)刚体&lt;/p&gt;
&lt;p&gt;触发事件有下面这些&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法名&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;th&gt;作者的大白话&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OnTriggerEnter / OnTriggerEnter2D&lt;/td&gt;
&lt;td&gt;GameObject 与另一个 GameObject 碰撞时，Unity 会调用 OnTriggerEnter&lt;/td&gt;
&lt;td&gt;俩游戏对象开始碰撞的时候调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OnTriggerStay / OnTriggerStay2D&lt;/td&gt;
&lt;td&gt;在另一个对象位于附加到该对象的触发器碰撞体之内时, 每帧调用一次&lt;/td&gt;
&lt;td&gt;只要俩碰撞体还处于碰撞的状态, 每一帧就调用一次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OnTriggerExit / OnTriggerExit2D&lt;/td&gt;
&lt;td&gt;当另一个对象离开附加到当前对象的触发器碰撞体时调用&lt;/td&gt;
&lt;td&gt;俩游戏对象不再激情碰撞了就调用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;至于网上所讲的, 两物体都有碰撞器, 并且其中一个是刚体, 是因为 Unity3D 中没有静态刚体, 所以只要有一个刚体就能触发. 而在 2D 中, 静态的刚体并不会引发触发器事件.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;标注&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;3D 的事件与 2D 的事件是不能通用的, 如果你是 2D 游戏对象, 并且使用的 2D 碰撞器与刚体, 那么使用 OnCollision 和 OnTrigger 什么的方法时, 必须使用带有 2D 后缀的方法&lt;/li&gt;
&lt;li&gt;需要注意的是, 例如一个地面, 只有一个碰撞体而不包含任何刚体, 一个带有碰撞体和刚体的游戏对象可以站立在这个地面上, 并且能够正常的触发碰撞相关的事件.&lt;/li&gt;
&lt;li&gt;如果你在做一个地刺效果, 玩家站立时不断受到伤害, 但是 OnTriggerStay 可能不会一直触发, 默认是在 Enter 的 0.5s 后可以持续触发, 然后进入 &quot;Sleep&quot; 状态, 此时除非玩家移动, 才会再次触发 TriggerStay 的消息. 如果你不希望它进入睡眠状态, 那么就把对应的刚体中, Sleep mode 改为 Never sleep. 如果你要全局更改这个时间, 可以在 Project Settings 中的 Physics2D 中, 将 Time to Sleep 改为你想要的值.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;关于此文章任何错误信息或者有不足的地方, 欢迎评论区指正&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>[Unity] Awake 与 Start 的使用, 为什么将 GetComponent 逻辑放到 Awake 中</title><link>https://slimenull.com/posts/20220927141618/</link><guid isPermaLink="true">https://slimenull.com/posts/20220927141618/</guid><description>震惊, 在 Start 中初始化变量竟然会引发如此严重的问题! 性能下降, 逻辑异常, 到底是人性的扭曲还是道德的沦丧?</description><pubDate>Tue, 27 Sep 2022 14:16:18 GMT</pubDate><content:encoded>&lt;p&gt;如果你的 Unity 脚本是这么写的, 那么你需要注意了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using UnityEngine;

public class MyScript : MonoBehaviour
{
    SpriteRenderer spriteRenderer;
    void Start()
    {
        spriteRenderer = GetComponent&amp;lt;SpriteRenderer&amp;gt;();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Unity 管线&lt;/h3&gt;
&lt;p&gt;Unity 不是完全渲染完 A 然后再去渲染 B 的, 而是先渲染 A 的一部分, 然后渲染 B 的一部分, 再渲染其他的一部分.&lt;/p&gt;
&lt;p&gt;然后再次渲染 A 的一部分, 渲染 B 的一部分, 渲染其他的一部分&lt;/p&gt;
&lt;p&gt;换做脚本, 假设有 ABC 三个对象, 执行的顺序应该是.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A.Method1
B.Method1
C.Method1

A.Method2
B.Method2
C.Method2

A.Method3
B.Method3
C.Method3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Awake 和 Start&lt;/h3&gt;
&lt;p&gt;Awake  和 Start 就相当于上面的 Method1, Method2, 所有对象的 Awake 都被调用一遍后, 然后再调用所有对象的 Start&lt;/p&gt;
&lt;h3&gt;不同对象顺序&lt;/h3&gt;
&lt;p&gt;Unity 是单线程, 所以 ABC 的 Method1 这种, 也是依次调用的, 也就是说, A 在调用 Method1 的时候, B 的 Method1 还没有被调用, 只能 A 的执行完之后轮到 B, 然后轮到 C&lt;/p&gt;
&lt;h3&gt;未初始化的变量&lt;/h3&gt;
&lt;p&gt;如果你把一些变量的初始化放在 Start 中, 那么在调用这个对象的 Start 前, 它的这些变量, 是完全没有初始化的, 为 null&lt;/p&gt;
&lt;h3&gt;引用未初始化的变量&lt;/h3&gt;
&lt;p&gt;如果你在某个对象没有调用 Start 前, 也就是它的变量未初始化时, 就调用这个对象的脚本暴露出来的一些公开方法, 例如依赖于 spriteRenderer  的 &quot;角色姿态变换&quot;, 那么, 由于 spriteRenderer 没有初始化, 你的程序就引发 NullReferenceException 了&lt;/p&gt;
&lt;p&gt;假设 A 的 Start 中需要调用 B 的 &quot;MethodQwq&quot;, 在 B 的 MethodQwq 中, 用到了 B 类的字段 &quot;spriteRenderer&quot;, 但是, spriteRenderer 是在 B 的 Start 方法中被赋值的&lt;/p&gt;
&lt;p&gt;当 A Start 执行时, 此时 B 的 Start 还没执行, B 的 spriteRenderer 为 null&lt;/p&gt;
&lt;p&gt;A 的 Start 调用 B 的 MethodQwq, 因为 spriteRenderer 为空, 所以引发了 NullReferenceException&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;统一在 Awake 方法中做变量的初始化, 统一在 Start 中做逻辑的开始执行. 这样在 Start 调用时, 所有对象的 Awake 已经调用过, 变量也全部初始化, 所以不会引发上述问题&lt;/p&gt;
</content:encoded></item><item><title>在 System.Text.Json 中使用构造函数进行反序列化</title><link>https://slimenull.com/posts/20220914131137/</link><guid isPermaLink="true">https://slimenull.com/posts/20220914131137/</guid><description>使用 System.Text.Json 时, 不可变类型的反序列化</description><pubDate>Wed, 14 Sep 2022 13:11:37 GMT</pubDate><content:encoded>&lt;h1&gt;在 System.Text.Json 中使用构造函数进行反序列化&lt;/h1&gt;
&lt;p&gt;有的时候, 我们希望一些类型在实例化之后, 就无法更改, 也就是说, 属性只读, 但是如果这样, System.Text.Json 就无法对属性进行赋值, 可以这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class UserModel
{
    public static string Username { get; set; }
    public static string Password { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改为:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class UserModel
{
    public UserModel(string username, string password)
    {
        Username = username;
        Password = password;
    }

    public static string Username { get; }
    public static string Password { get; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里, 我们暴露了一个公共的构造函数, 传入 username 与 password, 并对只读属性进行初始化. 这里, 构造函数的参数名和属性名必须是一一对应的, 允许大小写不同.&lt;/p&gt;
&lt;p&gt;需要注意的是, 如果你使用 JsonPropertyName 对 JSON 属性进行重命名, 你也必须保证构造函数参数名和属性名一致, 否则 System.Text.Json 将无法进行反序列化. 示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class UserModel
{
    public UserModel(string username, string password)
    {
        Username = username;
        Password = password;
    }

    [JsonPropertyName(&quot;user&quot;)]
    public static string Username { get; }

    [JsonPropertyName(&quot;pwd&quot;)]
    public static string Password { get; }    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外, 如果你的属性类型和参数类型不一致, 例如构造函数要求传入 string, 而属性是 Uri 的时候, 也是不允许的, System.Text.Json 无法识别它. 示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class UserModel
{
    public UserModel(string username, string password, string address)
    {
        Username = username;
        Password = password;
        Address = new Uri(address);
    }

    [JsonPropertyName(&quot;user&quot;)]
    public static string Username { get; }

    [JsonPropertyName(&quot;pwd&quot;)]
    public static string Password { get; }
    
    // 这里与构造函数不同, 此类型无法正常反序列化
    [JsonPropertyName(&quot;addr&quot;)]
    public static Uri Address { get; }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>2022年QQ微信内置浏览器 UA 标识, Header 获取</title><link>https://slimenull.com/posts/20220301174303/</link><guid isPermaLink="true">https://slimenull.com/posts/20220301174303/</guid><description>UA 标识QQ:Mozilla/5.0 (Linux; Android 11; Redmi Note 8 Pro Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/89.0.4389.72 MQQBrowser/6.2 TBS/045913 Mobile Safari/537.36 V1_AND_SQ_8.8.68_2538_YYB_D A_8086800 QQ/8.8.68.726</description><pubDate>Tue, 01 Mar 2022 17:43:03 GMT</pubDate><content:encoded>&lt;h2&gt;UA 标识&lt;/h2&gt;
&lt;p&gt;QQ:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Mozilla/5.0 (Linux; Android 11; Redmi Note 8 Pro Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/89.0.4389.72 MQQBrowser/6.2 TBS/045913 Mobile Safari/537.36 V1_AND_SQ_8.8.68_2538_YYB_D A_8086800 QQ/8.8.68.7265 NetType/WIFI WebP/0.3.0 Pixel/1080 StatusBarHeight/76 SimpleUISwitch/1 QQTheme/2971 InMagicWin/0 StudyMode/0 CurrentMode/1 CurrentFontScale/1.0 GlobalDensityScale/0.9818182 AppId/537112567 Edg/98.0.4758.102&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;微信:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[Mozilla/5.0 (Linux; Android 11; Redmi Note 8 Pro Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.99 XWEB/3185 MMWEBSDK/20211001 Mobile Safari/537.36 MMWEBID/6210 MicroMessenger/8.0.16.2040(0x2800105F) Process/toolsmp WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64] Edg/98.0.4758.102&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;获取&lt;/h2&gt;
&lt;p&gt;以上信息是在 2022 年 3 月获取的, 如果软件升级了版本, UA 变更导致这些信心不再有效, 你可以手动获取这些信息&lt;/p&gt;
&lt;p&gt;使用 golang 编写一个简单的服务端程序, 监听 8000 端口, 然后把 headers 和 cookies 全部打印出来.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
    &quot;net/http&quot;
)

func main() {
    http.HandleFunc(&quot;/&quot;, func(writer http.ResponseWriter, request *http.Request) {
        writer.Write([]byte(&quot;========== Headers ==========\n&quot;))
        for k, v := range request.Header {
            writer.Write([]byte(fmt.Sprintf(&quot;%s: %s;\n&quot;, k, v)))
        }
        writer.Write([]byte(&quot;========== Cookies ==========\n&quot;))
        for _, v := range request.Cookies() {
            writer.Write([]byte(fmt.Sprintf(&quot;%s - %s: %s;\n&quot;, v.Domain, v.Name, v.Value)))
        }
    })
    http.ListenAndServe(&quot;:8000&quot;, nil)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;手机连接到电脑所在局域网, 然后使用QQ微信访问电脑的 8000 端口即可看到 Headers 和 Cookies 了.&lt;/p&gt;
</content:encoded></item><item><title>[C#] 如何优雅的解决 DBNull 问题</title><link>https://slimenull.com/posts/20210714233802/</link><guid isPermaLink="true">https://slimenull.com/posts/20210714233802/</guid><description>什么是 DBNull 问题指从数据库取出数据时, 数据为空, 表现为 DBNull 无法转换为其他类型异常示例:// reader 为 DbDataReaderDateTime value = reader.GetDateTime(0);  // 在这里如果数据为空, 则会抛出异常普通的解决方式:DateTime value = reader.IsDBNull(0) ? default(DateTime) : reader.GetDateTime(0);优雅而又牛啤的解决方式:// 新建.</description><pubDate>Wed, 14 Jul 2021 23:38:02 GMT</pubDate><content:encoded>&lt;ol&gt;
&lt;li&gt;什么是 DBNull 问题
指从数据库取出数据时, 数据为空, 表现为 DBNull 无法转换为其他类型&lt;/li&gt;
&lt;li&gt;异常示例:&lt;pre&gt;&lt;code&gt;// reader 为 DbDataReader
DateTime dt = reader.GetDateTime(0);  // 在这里如果数据为空, 则会抛出异常
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;普通的解决方式:&lt;pre&gt;&lt;code&gt;DateTime dt = reader.IsDBNull(0) ? default(DateTime) : reader.GetDateTime(0);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;优雅而又牛啤的解决方式:&lt;pre&gt;&lt;code&gt;// 新建一个文件, 写入以下内容
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace NullLib.DbHelper
{
    public static class DbExt
    {
        public static T Cast&amp;lt;T&amp;gt;(this object obj)    // 使用 default 默认值
        {
            if (obj is T rst)
                return rst;
            return default;
        }
        public static T Cast&amp;lt;T&amp;gt;(this object obj, T dft)   // 用户指定默认值
        {
            if (obj is T rst)
                return rst;
            return dft;
        }
        public static object CastDb(this object obj)  // 将CLR对象转换为数据库类型
        {
            if (obj is null)
                return DBNull.Value;
            return obj;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
然后在取值的时候这样做:&lt;pre&gt;&lt;code&gt;DateTime dt = reader[0].Cast&amp;lt;DateTime&amp;gt;();  // 如果取值失败则会返回 default(DateTime)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>[C#] 使用 NullLib.TickAnimation.Drawing 实现 WinForm 过渡动画</title><link>https://slimenull.com/posts/20210630024505/</link><guid isPermaLink="true">https://slimenull.com/posts/20210630024505/</guid><description>NullLib.TickAnimation 用于实现对一个对象的属性(Property), 以指定的计时函数, 在一定时间内从一个值, 平滑的过渡到另一个值.例如你可以使用它将一个 WinForm 窗体的 Bounds 从一个值过渡到另一个值以实现窗体位置与尺寸的过渡动画. 下面是一个简单示例:快速开始首先, 创建一个 WinForm 项目用于测试, 向主窗体中添加一个按钮, 并为按钮的点击事件添加事件处理器在解决方案管理器中右击项目, 选择 “管理 nuget 程序包”, 然后安</description><pubDate>Wed, 30 Jun 2021 02:45:05 GMT</pubDate><content:encoded>&lt;p&gt;NullLib.TickAnimation 用于实现对一个对象的属性(Property), 以指定的计时函数, 在一定时间内从一个值, 平滑的过渡到另一个值. NullLib.TickAnimation.Drawing 基于 NullLib.TickAnimation, 并提供了关于绘画类型的过渡动画, 例如 点, 尺寸, 矩形区域, 颜色&lt;/p&gt;
&lt;p&gt;例如你可以使用它将一个 WinForm 窗体的 Bounds 从一个值过渡到另一个值以实现窗体位置与尺寸的过渡动画. 下面是一个简单示例:&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=&quot;center&quot;&amp;gt;
&amp;lt;a href=&quot;https://www.picbed.cn/images/2021/06/30/nMsu3Dxxpt.gif&quot; target=&quot;_blank&quot;&amp;gt;
&amp;lt;img src=&quot;https://www.picbed.cn/images/2021/06/30/nMsu3Dxxpt.gif&quot; alt=&quot;点击此处跳转到图片源页面&quot;/&amp;gt;
&amp;lt;/a&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先, 创建一个 WinForm 项目用于测试, 向主窗体中添加一个按钮, 并为按钮的点击事件添加事件处理器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在解决方案管理器中右击项目, 选择 &quot;管理 nuget 程序包&quot;, 然后安装 NullLib.TickAnimation.Drawing (它会自动安装依赖项 NullLib.TickAnimation)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;转到主窗体的代码, 在 using 部分添加 &lt;code&gt;using NullLib.TickAnimation&lt;/code&gt;, 这样我们就可以直接使用关于动画的一些类了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编辑刚刚添加的按钮的点击事件处理器代码, 添加以下内容:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Rectangle workingArea = Screen.PrimaryScreen.WorkingArea;        // 获取主屏幕的工作区矩形
DrawingTickAnimator animator = new DrawingTickAnimator(new SineTicker(), this, nameof(Bounds));   // 创建动画控制器实例
animator.SetPropertySetter((setAction) =&amp;gt; Invoke(setAction));    // 解决窗体程序的跨线程资源访问问题
animator.SetTickDelay(1);                                        // 在每一帧后进行 1ms 的延时
animator.Animate(workingArea, 200);                              // 进行动画, 时间是 200ms
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行程序, 点击按钮, 查看效果&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;本库已在 GitHub 开源, &lt;a href=&quot;https://github.com/SlimeNull/NullLib.TickAnimation&quot;&gt;github.com/SlimeNull/NullLib.TickAnimation&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;基本原理&lt;/h2&gt;
&lt;p&gt;在 NullLib.TickAnimator 中, ITickAnimator 是最基本的, 用于运行动画的接口, 它依赖于 ITicker 来提供计时函数, ITIcker 可表示运动曲线, 不同的类有不同的特征, 例如使用 BackTicker 的 ITickAnimator 进行动画时将具有回弹效果, 使用 BounceTicker 的 ITickAnimator 进行动画时将具有弹跳效果.&lt;/p&gt;
&lt;p&gt;其中的技术细节, 可参考 CSS3 过渡中的 timing-function, WPF 窗体程序中的 EasingFunction. 另外, 也推荐对贝塞尔曲线作基本了解, 这样, 你可以通过库中提供的 CubicBezierTicker 来创建自定义的三次贝塞尔曲线计时函数, 进而实现自定义动画效果.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;推荐的在线三次贝塞尔曲线编辑工具: &lt;a href=&quot;https://cubic-bezier.com/&quot;&gt;Cubic-Bezier&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;类型&lt;/h2&gt;
&lt;h3&gt;- ITicker&lt;/h3&gt;
&lt;p&gt;ITicker 是动画计时函数, 它分为两种, 贝塞尔曲线函数以及原生函数, 其中贝塞尔曲线是可以由用户指定控制点从而控制曲线形状的, 原生函数是一些定义好的, 遵循 ITicker 接口的函数&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;贝塞尔曲线函数: 分两种, 三次贝塞尔曲线(CubicBezierTicker)与二次贝塞尔曲线(QuadraticBezierTicker), 你可以手动指定控制点, 也可以使用默认提供的一些曲线, 例如 Ease, EaseIn, EaseOut, EaseInOut, InSine, OutSine 等.&lt;/p&gt;
&lt;p&gt;指定 EaseInOut 的方式是: 指定 CubicBezierCurve.Ease 与 EasingMode.EaseInOut, 同理, 指定 InOutBack 的方式是: CubicBezierCurve.Back 与 EasingMode.EaseInOut, 需要知道的是, 这些曲线都是源自于 CSS3 内置曲线(Ease, EaseIn, EaseOut, EaseInOut, Linear) 以及 Microsoft Edge 浏览器调试工具中的预设曲线(InSIne, OutSine, InOutSine, InBack, OutBack, ......)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;原生函数: 它们都是从 WPF 中移植过来的, 例如 BounceTicker, 它来自 WPF 中的 BounceEase, 并且与它算法完全一致, 关于它们的使用, 我建议查阅 WPF 动画缓动函数的相关文档.&lt;/p&gt;
&lt;p&gt;最简单的, 使用 BackTicker 函数就是直接实例化一个对象, 在这里需要提到的是, 关于 BackTicker 实例的属性默认值也是与 WPF 一致的, 虽然 BackTicker 和 CubicBezierCurve.Back 都是表示带有回弹效果的曲线, 但是两者并不相等, 贝塞尔曲线函数和原生函数是截然不同的.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;关于 ITicker 的更多信息, 我建议查阅源码, 或者在 Visual Studio 中使用对象浏览器概览所有类型.&lt;/p&gt;
&lt;h3&gt;- ITickAnimator&lt;/h3&gt;
&lt;p&gt;ITickAnimator 是驱动动画的最主要的类, 默认包含的 TickAnimator 中支持 double float int 三种数字类型的动画处理,  它其中还包括了对于动画同时进行时的中断处理, 当然, 如果你要使用针对于绘画的动画处理, 例如点, 尺寸, 矩形区域, 颜色这类, 可以使用 DrawingTickAnimator (nuget包: NullLib.TickAnimator.Drawing, 命名空间: NullLib.TickAnimator)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;TickAnimator 的动画处理中包含这样一点: 当一个动画还未结束就启动另一个动画, 那么第一个动画会自动停止以保证不会造成冲突, 这是因为一个 TickAnimator 应该针对于唯一一个属性, 但针对同一个属性的两个动画同时启动, 将会造成严重的冲突问题. (这里需要提一下, 如果你要同时对一个控件进行位置过渡和尺寸过渡, 我不推荐你为 Location 和 Size 属性分别创建一个 ITickAnimator 实例, WinForm 的内部实现问题仍然会导致冲突, 更安全的做法是为 Bounds 属性创建 ITickAnimator 并且使用针对 Rectangle 的过渡)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;- TickAnimationProc&lt;/h3&gt;
&lt;p&gt;TickAnimationProc 是 NullLib.TickAnimation 中对于动画驱动的最核心部分, 它提供了最为基本的动画实现方式, 通过指定 ITicker(计时函数), Fucn&amp;lt;double, T&amp;gt;(tick picker用于从动画进度中取得对应类型的值), TimeSpan(动画的时间间隔), Func&amp;lt;T, bool&amp;gt;(动画每一帧的回调函数, 返回值表示动画是否应该继续进行), 这四个参数来运行最基本的动画. 但是一般的不推荐使用这个, 因为其中没有像 TickAnimator 的互斥措施.&lt;/p&gt;
&lt;h3&gt;- TickAnimatorBase&lt;/h3&gt;
&lt;p&gt;TickAnimator 的基类, 其中包含了 TickAnimator 基础功能的实现, 例如对于值过渡的方法封装&lt;/p&gt;
&lt;h2&gt;自定义类型&lt;/h2&gt;
&lt;p&gt;如果你要自定义 ITicker 实现类或者 ITickAnimator 实现类, 则需要遵守下面的一些规范:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;对于 ITicker 的实现, 必须保证对于方法 CalcTick(double x), x 从 0 到 1 时, 返回值也是从 0 到 1, 即: CalcTick(0) 应该返回 0, CalcTick(1) 应该返回 1.&lt;/p&gt;
&lt;p&gt;并且, 如果你要创建更高阶的贝塞尔曲线函数, 建议继承 BezierTickerBase, 如果你要创建一些遵循 WPF 原生过渡函数规则的自定义函数, 必须继承 FuncTickerBase 并在字类中重写 CalcInTick(double x) 方法.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于 ITickAnimator 的实现, 推荐的方式是直接继承 TickAnimator, 并且在实现 Animate 和 SyncAnimate 方法时, 应该通过调用 TickAnimatorBase 的 Animate 和 SyncAnimate 方法来实现动画, 而获取对应属性的当前值, 不应该使用 prop.GetValue 方法, 而是使用 TickAnimatorBase 的 GetPropertyValue 方法. 下面是 TickAnimator 中 double 的动画实现:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Task Animate(float start, float end, int dur)
{
    double diff = end - start;
    return Animate((t) =&amp;gt; (float)(start + diff * t), TimeSpan.FromMilliseconds(dur));    // 内部调用基类 TickAnimatorBase 的方法
}
public Task Animate(double end, int dur) =&amp;gt; Animate(GetPropertyValue&amp;lt;double&amp;gt;(), end, dur);    // 从当前值开始执行动画, 按照规
public ITickAnimator&amp;lt;double&amp;gt; SyncAnimate(double start, double end, int dur)                   // 范, 使用 GetPropertyValue 方法
{
    double diff = end - start;
    SyncAnimate((t) =&amp;gt; (double)(start + diff * t), TimeSpan.FromMilliseconds(dur));      // SyncAnimate 也一样
    return this;
}
public ITickAnimator&amp;lt;double&amp;gt; SyncAnimate(double end, int dur) =&amp;gt; SyncAnimate(GetPropertyValue&amp;lt;double&amp;gt;(), end, dur);  // 一致
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>[C#] 使用 NullLib.CommandLine 分析命令行字符串并执行与代码中定义的方法</title><link>https://slimenull.com/posts/20210629215451/</link><guid isPermaLink="true">https://slimenull.com/posts/20210629215451/</guid><description>摘要介绍通过命令行字符串来方便快捷的调用 C# 中定义的方法准备添加 nuget 包: NullLib.CommandLine使用方式首先, 在 NullLib.CommandLine 中用于调用方法的最基本类型是 CommandObject, 它包含了方法的各种信息, 例如 MethodInfo, ParameterInfo, 以及属性.然后, 你需要定义一个包含要调用方法的类, 在这个类中, 每一个将被调用的方法都应该有一个 Command 属性, 之后我们将用这个类型实例化一个 Com</description><pubDate>Tue, 29 Jun 2021 21:54:51 GMT</pubDate><content:encoded>&lt;h1&gt;摘要介绍&lt;/h1&gt;
&lt;p&gt;通过命令行字符串来方便快捷的调用 C# 中定义的方法&lt;/p&gt;
&lt;h2&gt;准备&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;添加 nuget 包: NullLib.CommandLine&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;使用方式&lt;/h2&gt;
&lt;p&gt;首先, 在 &lt;strong&gt;NullLib.CommandLine&lt;/strong&gt; 中用于调用方法的最基本类型是 &lt;code&gt;CommandObject&lt;/code&gt;, 它包含了方法的各种信息, 例如 &lt;code&gt;MethodInfo&lt;/code&gt;, &lt;code&gt;ParameterInfo&lt;/code&gt;, 以及属性.&lt;/p&gt;
&lt;p&gt;然后, 你需要定义一个包含要调用方法的类, 在这个类中, 每一个将被调用的方法都应该有一个 &lt;code&gt;Command&lt;/code&gt; 属性, 之后我们将用这个类型实例化一个 &lt;code&gt;CommandObject&lt;/code&gt; 实例.&lt;/p&gt;
&lt;p&gt;一个示例类型:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AppCommands
{
    [Command]
    public void HelloWorld()
    {
        Console.WriteLine(&quot;Hello world!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实例化一个 &lt;code&gt;CommandObject&lt;/code&gt; 对象, 然后循环执行指令.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System;
using NullLib.CommandLine;

class Program
{
    static CommandObject&amp;lt;AppCommands&amp;gt; AppCommandObject = new CommandObject&amp;lt;AppCommands&amp;gt;();   // 实例化一个 CommandObject 对象
    static void Main(string[] args)
    {
        Console.WriteLine(&quot;Now input commands.&quot;);
        while (true)
        {
            Console.Write(&quot;&amp;gt;&amp;gt;&amp;gt; &quot;);          // 提示符
            string cmdline = Console.ReadLine();
            if (!AppCommandObject.TryExecuteCommand(cmdline, out var result))
            {
                if (result != null)             // 如果一个方法没有返回值, 则结果是 null.
                    Console.WriteLine(result);
            }
            else
            {
                Console.WriteLine(&quot;Command execute failed.&quot;);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行程序, 并输入指令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Now input commands.
&amp;gt;&amp;gt;&amp;gt; HelloWorld
Hello world!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为每一个参数指定 &lt;code&gt;ArgumentConverter&lt;/code&gt; 以传递参数到方法.&lt;/p&gt;
&lt;p&gt;那么, 我们再试试将这些方法添加到 &lt;code&gt;AppCommands&lt;/code&gt; 中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Command(typeof(FloatArguConverter), typeof(FloatArguConverter))]      // NullLib.CommandLine 中的内置 ArgumentConverter
public float Plus(float a, float b)
{
    return a + b;
}
[Command(typeof(FloatArguConverter))]        // 如果跟着的转换器与上一个是一样的, 那么你可以忽略它们
public float Mul(float a, float b)
{
    return a * b;
}
[Command(typeof(DoubleArguConverter))]
public double Log(double n, double newBase = Math.E)    // 你也可以使用可选参数
{
    return Math.Log(n, newBase);
}
[Command(typeof(ForeachArguConverter&amp;lt;FloatArguConverter&amp;gt;))]   // 数组中的每一个字符串都将被 FloatArguConverter 转换
public float Sum(params float[] nums)                 // 可变参数的方法也是受支持的
{
    float result = 0;
    foreach (var i in nums)
        result += i;
    return result;
}
[Command(typeof(ArgutConverter))]        // 如果不需要做任何转换, 则可以指定一个 &apos;ArguConverter&apos;
public void Print(string txt)
{
    Console.WriteLine(txt);
}
[Command]                                // 默认的转换器是 &apos;ArguConverter&apos;, 在这种情况下你可以忽略它们
public bool StringEquals(string txt1, string txt2)   // 或者指定 &apos;null&apos; 来使用上一个转换器(在这里指 ArguConverter)
{
    return txt1.Equals(txt2);
}
[Command(typeof(EnumArguConverter&amp;lt;ConsoleColor&amp;gt;))]   // EnumConverter 可以用来将字符串自动转换为枚举类型
public void SetBackground(ConsoleColor color)
{
    Console.BackgroundColor = color;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run and input:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Now input commands.
&amp;gt;&amp;gt;&amp;gt; Plus 1 1
2
&amp;gt;&amp;gt;&amp;gt; Mul 2 4
8
&amp;gt;&amp;gt;&amp;gt; Log 8 2
3
&amp;gt;&amp;gt;&amp;gt; Log 8
2.07944154167984
&amp;gt;&amp;gt;&amp;gt; Sum 1 2 3 4
10
&amp;gt;&amp;gt;&amp;gt; Print &quot;一些文本\t转义字符也是受支持的&quot;
一些文本    转义字符也是受支持的
&amp;gt;&amp;gt;&amp;gt; StringEquals qwq awa
False
&amp;gt;&amp;gt;&amp;gt; SetBackground White
&amp;gt;&amp;gt;&amp;gt; SetBackground 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;更多&lt;/h2&gt;
&lt;p&gt;通过这个库, 你可以通过命令行字符串方便快捷的调用 C# 中定义的方法, 甚至于通过一些方法实现简单的, 类似于 CMD 命令提示符的命令解释器, 完整文档,&lt;/p&gt;
&lt;h1&gt;NullLib.CommandLine&lt;/h1&gt;
&lt;p&gt;通过命令行字符串来方便快捷的调用 C# 中定义的方法&lt;/p&gt;
&lt;h2&gt;使用方式&lt;/h2&gt;
&lt;p&gt;首先, 在 &lt;strong&gt;NullLib.CommandLine&lt;/strong&gt; 中用于调用方法的最基本类型是 &lt;code&gt;CommandObject&lt;/code&gt;, 它包含了方法的各种信息, 例如 &lt;code&gt;MethodInfo&lt;/code&gt;, &lt;code&gt;ParameterInfo&lt;/code&gt;, 以及属性.&lt;/p&gt;
&lt;p&gt;然后, 你需要定义一个包含要调用方法的类, 在这个类中, 每一个将被调用的方法都应该有一个 &lt;code&gt;Command&lt;/code&gt; 属性, 之后我们将用这个类型实例化一个 &lt;code&gt;CommandObject&lt;/code&gt; 实例.&lt;/p&gt;
&lt;p&gt;一个示例类型:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AppCommands
{
    [Command]
    public void HelloWorld()
    {
        Console.WriteLine(&quot;Hello world!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实例化一个 &lt;code&gt;CommandObject&lt;/code&gt; 对象, 然后循环执行指令.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System;
using NullLib.CommandLine;

class Program
{
    static CommandObject&amp;lt;AppCommands&amp;gt; AppCommandObject = new CommandObject&amp;lt;AppCommands&amp;gt;();   // 实例化一个 CommandObject 对象
    static void Main(string[] args)
    {
        Console.WriteLine(&quot;Now input commands.&quot;);
        while (true)
        {
            Console.Write(&quot;&amp;gt;&amp;gt;&amp;gt; &quot;);          // 提示符
            string cmdline = Console.ReadLine();
            if (!AppCommandObject.TryExecuteCommand(cmdline, out var result))
            {
                if (result != null)             // 如果一个方法没有返回值, 则结果是 null.
                    Console.WriteLine(result);
            }
            else
            {
                Console.WriteLine(&quot;Command execute failed.&quot;);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行程序, 并输入指令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Now input commands.
&amp;gt;&amp;gt;&amp;gt; HelloWorld
Hello world!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为每一个参数指定 &lt;code&gt;ArgumentConverter&lt;/code&gt; 来以传递参数到方法.&lt;/p&gt;
&lt;p&gt;那么, 我们再试试将这些方法添加到 &lt;code&gt;AppCommands&lt;/code&gt; 中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Command(typeof(FloatArguConverter), typeof(FloatArguConverter))]      // NullLib.CommandLine 中的内置 ArgumentConverter
public float Plus(float a, float b)
{
    return a + b;
}
[Command(typeof(FloatArguConverter))]        // 如果跟着的转换器与上一个是一样的, 那么你可以忽略它们
public float Mul(float a, float b)
{
    return a * b;
}
[Command(typeof(DoubleArguConverter))]
public double Log(double n, double newBase = Math.E)    // 你也可以使用可选参数
{
    return Math.Log(n, newBase);
}
[Command(typeof(ForeachArguConverter&amp;lt;FloatArguConverter&amp;gt;))]   // 数组中的每一个字符串都将被 FloatArguConverter 转换
public float Sum(params float[] nums)                 // 可变参数的方法也是受支持的
{
    float result = 0;
    foreach (var i in nums)
        result += i;
    return result;
}
[Command(typeof(ArgutConverter))]        // 如果不需要做任何转换, 则可以指定一个 &apos;ArguConverter&apos;
public void Print(string txt)
{
    Console.WriteLine(txt);
}
[Command]                                   // 默认的转换器是 &apos;ArguConverter&apos;, 在这种情况下你可以忽略它们
public bool StringEquals(string txt1, string txt2)   // 或者指定 &apos;null&apos; 来使用上一个转换器(在这里指 ArguConverter)
{
    return txt1.Equals(txt2);
}
[Command(typeof(EnumArguConverter&amp;lt;ConsoleColor&amp;gt;))]   // EnumConverter 可以用来将字符串自动转换为枚举类型
public void SetBackground(ConsoleColor color)
{
    Console.BackgroundColor = color;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run and input:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Now input commands.
&amp;gt;&amp;gt;&amp;gt; Plus 1 1
2
&amp;gt;&amp;gt;&amp;gt; Mul 2 4
8
&amp;gt;&amp;gt;&amp;gt; Log 8 2
3
&amp;gt;&amp;gt;&amp;gt; Log 8
2.07944154167984
&amp;gt;&amp;gt;&amp;gt; Sum 1 2 3 4
10
&amp;gt;&amp;gt;&amp;gt; Print &quot;一些文本\t转义字符也是受支持的&quot;
一些文本    转义字符也是受支持的
&amp;gt;&amp;gt;&amp;gt; StringEquals qwq awa
False
&amp;gt;&amp;gt;&amp;gt; SetBackground White
&amp;gt;&amp;gt;&amp;gt; SetBackground 0
&amp;gt;&amp;gt;&amp;gt; Print &quot;你可以通过一个枚举类型的名字或整数值来进行转换&quot;
你可以通过一个枚举类型的名字或整数值来进行转换
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Types&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;CommandAttribute:&lt;/p&gt;
&lt;p&gt;可以通过命令行字符串执行的方法必须有一个 &lt;code&gt;CommandAttribute&lt;/code&gt; 属性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CommandObject:&lt;/p&gt;
&lt;p&gt;用于使用命令行字符串调用方法的类&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CommandInvoker:&lt;/p&gt;
&lt;p&gt;帮助通过 &lt;code&gt;IArguments&lt;/code&gt; 调用方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CommandParser:&lt;/p&gt;
&lt;p&gt;帮助分析命令行字符串以调用方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ArgumentConverter:&lt;/p&gt;
&lt;p&gt;继承 &lt;code&gt;IArgumentConverter&lt;/code&gt; 接口, 抽象类, 应该被自定义转换器继承&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ArgumentConverterManager:&lt;/p&gt;
&lt;p&gt;帮助快速创建 &lt;code&gt;ArgumentConverter&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CommandLineSegment:&lt;/p&gt;
&lt;p&gt;命令行字符串的构成部分&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ArgumentParser:&lt;/p&gt;
&lt;p&gt;帮助将 &lt;code&gt;CommandLineSegment&lt;/code&gt; 分析为 &lt;code&gt;IArgument&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;CommandLineSegment&lt;/h2&gt;
&lt;p&gt;CommandLineSegment 是命令行字符串的组成部分&lt;/p&gt;
&lt;p&gt;例如, 在 &lt;code&gt;myprogram param1 &quot;param2&quot;&lt;/code&gt; 中, 有三个 CommandLineSegment:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;{Quoted: false, Content: &quot;myprogram&quot;}&lt;/li&gt;
&lt;li&gt;{Quoted: false, Content: &quot;param1&quot;}&lt;/li&gt;
&lt;li&gt;{Quoted: true, Content: &quot;\&quot;param2\&quot;&quot;}&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;将命令行字符串分割为 CommandLineSegment[], 使用 &lt;code&gt;CommandParser.SplitCommandLine(string str)&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Argument&lt;/h2&gt;
&lt;p&gt;命令的参数, 可有名称, 继承 &lt;code&gt;IArgument&lt;/code&gt;, 当调用方法时, 将会被传递&lt;/p&gt;
&lt;h2&gt;ArgumentParser&lt;/h2&gt;
&lt;p&gt;下面是所有内置的 &lt;code&gt;ArgumentParsers&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;ArguParser:&lt;/p&gt;
&lt;p&gt;可将任何部分分析为一个 &lt;code&gt;Argument&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IdentifierArguParser:&lt;/p&gt;
&lt;p&gt;可将一个标识符部分分析为一个 &lt;code&gt;Argument&lt;/code&gt;. 对应的正则表达式是 &lt;em&gt;&quot;{A-Za-z_}{A-Za-z0-9_}*&quot;&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;StringArguParser:&lt;/p&gt;
&lt;p&gt;可以将任何 Quoted(被双引号包围的) 部分分析为 &lt;code&gt;Argument&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FieldArguParser:&lt;/p&gt;
&lt;p&gt;可以将像 &amp;lt;u&amp;gt;&lt;em&gt;name=value&lt;/em&gt;&amp;lt;/u&amp;gt; 的一个部分, 或者像 &amp;lt;u&amp;gt;&lt;em&gt;name= value&lt;/em&gt;&amp;lt;/u&amp;gt; 的两个部分分析为一个 &lt;code&gt;Argument&lt;/code&gt;, 另外, 你可以指定分隔符, 默认分隔符是 &apos;=&apos;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PropertyArguParser:&lt;/p&gt;
&lt;p&gt;可以将像 &amp;lt;u&amp;gt;&lt;em&gt;-name value&lt;/em&gt;&amp;lt;/u&amp;gt; 的两个部分分析为 &lt;code&gt;Argument&lt;/code&gt;, 并且你也可以指定 &amp;lt;u&amp;gt;&lt;em&gt;name&lt;/em&gt;&amp;lt;/u&amp;gt; 的起始字符串, 默认是 &quot;-&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;ArgumentConverter&lt;/h2&gt;
&lt;p&gt;Here is all build-in &lt;code&gt;ArgumentConverter&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;ArguConverter:&lt;/p&gt;
&lt;p&gt;不会做任何转换而直接返回源值的默认的 &lt;code&gt;ArgumentConverter&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BoolArguConverter:&lt;/p&gt;
&lt;p&gt;帮助转换到布尔值, 使用 bool.Parse 和 bool.TryParse&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CharArguConverter:&lt;/p&gt;
&lt;p&gt;帮助转换到字符, 仅当字符串有唯一一个字符时, 返回这个字符, 否则转换失败&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ByteArguConverter:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ShortArguConverter:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IntArguConverter:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LongArguConverter:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FloatArguConverter:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DoubleArguConverter:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BigIntArguConverter:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DecimalArguConverter:&lt;/p&gt;
&lt;p&gt;上面提到的转换器均返回对应的数字类型, 并且它们都通过调用对应类型的 &lt;code&gt;Parse&lt;/code&gt; 和 &lt;code&gt;TryParse&lt;/code&gt; 方法实现转换&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;EnumArguConverter&amp;lt;T&amp;gt;:&lt;/p&gt;
&lt;p&gt;帮助转换到枚举类型, T 应该被指定为一个枚举类型. 它可以从枚举值的名称或数字值来转换&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ForeachArguConverter&amp;lt;TConverter&amp;gt;:&lt;/p&gt;
&lt;p&gt;帮助转换到一个数组, 仅用于可变参数(使用 &lt;code&gt;params&lt;/code&gt; 修饰), &lt;code&gt;TConverter&lt;/code&gt; 必须实现 &lt;code&gt;IArgumentConverter&lt;/code&gt; 接口, 每个值都将被指定的转换器转换, 最终得到一个对应类型的数组.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CharArrayArguConverter:&lt;/p&gt;
&lt;p&gt;帮助转换到字符数组, 它调用 &lt;code&gt;string.ToCharArray()&lt;/code&gt; 来进行转换.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;About ArgumentParser&lt;/h2&gt;
&lt;h3&gt;Custom Parser:&lt;/h3&gt;
&lt;p&gt;定义自定义的 &lt;code&gt;ArgumentParser&lt;/code&gt;, 你需要遵守下面的规则:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;实现 &lt;code&gt;IArgumentParser&lt;/code&gt; 接口&lt;/li&gt;
&lt;li&gt;在分析完毕后, 引用参数 &lt;code&gt;index&lt;/code&gt; 必须离开参数结果(IArgument)的区域, 例如, 在索引 3, 在你的自定义分析器中, 他将返回结果 (结果被成功分析), 并且分析结果来自于两个 &lt;code&gt;CommandLineSegment&lt;/code&gt;, 所以 &lt;code&gt;index&lt;/code&gt; 必须是 5 (在 3 和 4 之外).&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;About ArgumentConverter&lt;/h2&gt;
&lt;h3&gt;自定义 Converter:&lt;/h3&gt;
&lt;p&gt;定义自定义 &lt;code&gt;ArgumentConverter&lt;/code&gt; 的推荐方式是这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyConverter : ArgumentConverter&amp;lt;MyType&amp;gt;    // 继承 ArgumentConverter&amp;lt;T&amp;gt; 而不是 IArgumentConverter&amp;lt;T&amp;gt;
{
    public override MyType Convert(string argument)
    {
        // 你的代码
    }
    public override bool TryConvert(string argument, out MyType result)
    {
        // 你的代码
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继承 &lt;code&gt;ArgumentConverter&amp;lt;T&amp;gt;&lt;/code&gt; 而不是 &lt;code&gt;IArgumentConverter&amp;lt;T&amp;gt;&lt;/code&gt; 的原因是, 在 &lt;code&gt;ArgumentConverter&amp;lt;T&amp;gt;&lt;/code&gt; 中, 所有方法重载均调用这两个方法:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;T Convert(string argument);&lt;/li&gt;
&lt;li&gt;bool TryConverter(string argument, out T result);&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以如果继承 &lt;code&gt;ArgumentConverter&amp;lt;T&amp;gt;&lt;/code&gt;, 你只需要重写这两个方法.&lt;/p&gt;
&lt;h3&gt;Tips:&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;不要使用 new 表达式来创建一个 &lt;code&gt;ArgumentConverter&lt;/code&gt; 实例, 请使用 &lt;code&gt;ArgumentConverterManager.GetConverter&amp;lt;T&amp;gt;()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;当我调用 &lt;code&gt;CommandObject.ExecuteCommand(IArgumentParser[] parsers, string cmdline)&lt;/code&gt; 时, 某些分析器不能正常使用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 你必须以正确的顺序指定分析器, 否则就会这样:
CommandObject&amp;lt;AppCommands&amp;gt; myCmds = new CommandObject&amp;lt;AppCommands&amp;gt;();
myCmds.ExecuteCommand(new IArgumentParser[]
{
    new ArguParser(),                // 在这种情况下, FieldArguParser 和 PropertyArguParser 将不起作用.
    new FieldArguParser(),           // 这是因为 ArguParser 可以分析任何 CommandLineSegments, 所以你应该
    new PropertyArguParser()         // 将 ArguParser 作为最后一个分析器, 这样分析的时候就会先使用前两个
}, Console.ReadLine());
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>[C#] 使用 NAudio 实现音频可视化</title><link>https://slimenull.com/posts/20210509234918/</link><guid isPermaLink="true">https://slimenull.com/posts/20210509234918/</guid><description>预览:捕捉声卡输出:实现音频可视化, 第一步就是获得音频采样, 这里我们选择使用计算机正在播放的音频作为采样源进行处理:NAudio 中, 可以借助 WasapiLoopbackCapture 来进行捕捉:WasapiLoopbackCapture cap = new WasapiLoopbackCapture();cap.DataAvailable += (sender, e) =&gt;      // 录制数据可用时触发此事件, 参数中包含音频数据{    float[] allSam</description><pubDate>Sun, 09 May 2021 23:49:18 GMT</pubDate><content:encoded>&lt;h2&gt;预览:&lt;/h2&gt;
&lt;p&gt;最初版本:
&lt;img src=&quot;/images/20210509234816552.gif&quot; alt=&quot;preview&quot; /&gt;
更新:
&amp;lt;a href=&quot;https://sm.ms/image/TCABEJvFoDMrnaf&quot; target=&quot;_blank&quot;&amp;gt;&amp;lt;img src=&quot;https://i.loli.net/2021/06/25/TCABEJvFoDMrnaf.gif&quot; &amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;再次更新 (这次优化了算法):&lt;/p&gt;
&lt;p&gt;[video(video-meXn8OHU-1667173967679)(type-bilibili)(url-https://player.bilibili.com/player.html?aid=902039129)(image-https://img-blog.csdnimg.cn/img_convert/86ebcb2e350f1bce9383f671951f1fb7.jpeg)(title-[演示] C# NAudio Direct2D 音频可视化)]&lt;/p&gt;
&lt;h2&gt;捕捉声卡输出:&lt;/h2&gt;
&lt;p&gt;实现音频可视化, 第一步就是获得音频采样, 这里我们选择使用计算机正在播放的音频作为采样源进行处理:&lt;/p&gt;
&lt;p&gt;NAudio 中, 可以借助 WasapiLoopbackCapture 来进行捕捉:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WasapiLoopbackCapture cap = new WasapiLoopbackCapture();
cap.DataAvailable += (sender, e) =&amp;gt;      // 录制数据可用时触发此事件, 参数中包含音频数据
{
    float[] allSamples = Enumerable      // 提取数据中的采样
        .Range(0, e.BytesRecorded / 4)   // 除以四是因为, 缓冲区内每 4 个字节构成一个浮点数, 一个浮点数是一个采样
        .Select(i =&amp;gt; BitConverter.ToSingle(e.Buffer, i * 4))  // 转换为 float
        .ToArray();    // 转换为数组
    // 获取采样后, 在这里进行详细处理
}
cap.StartRecording();   // 开始录制
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;分离左右通道:&lt;/h2&gt;
&lt;p&gt;获取完采样后, 我们还需要对采样进行一点小处理, 因为捕获的数据是分通道的, 一般是左右声道:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 设定我们已经将刚刚的采样保存到了变量 AllSamples 中, 类型为 float[]
int channelCount = cap.WaveFormat.Channels;   // WasapiLoopbackCapture 的 WaveFormat 指定了当前声音的波形格式, 其中包含就通道数
float[][] channelSamples = Enumerable
    .Range(0, channelCount)
    .Select(channel =&amp;gt; Enumerable
        .Range(0, AllSamples.Length / channelCount)
        .Select(i =&amp;gt; AllSamples[channel + i * channelCount])
        .ToArray())
    .ToArray();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;取通道平均值&lt;/h2&gt;
&lt;p&gt;将采样分为一个个通道的采样后, 我们可以将其合并, 取平均值, 以便于绘制:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 设定我们已经将分开了的采样保存到了变量 ChannelSamples 中, 类型为 float[][]
// 例如通道数为2, 那么左声道的采样为 ChannelSamples[0], 右声道为 ChannelSamples[1]
float[] averageSamples = Enumerable
    .Range(0, AllSamples.Length / channelCount)
    .Select(index =&amp;gt; Enumerable
        .Range(0, channelCount)
        .Select(channel =&amp;gt; ChannelSamples[channel][index])
        .Average())
    .ToArray();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;绘制时域图象:&lt;/h2&gt;
&lt;p&gt;处理刚刚的采样后, 你可以直接将其作为数据绘制到窗口中, 这即是时域图象, 这里使用最简单的折线绘制.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 设定 g 为窗口的 Graphics 对象, windowHeight 为窗口的显示区域高度
// 设定通道采样平均值为 AverageSamples, 类型为 float[]
Point[] points = AverageSamples
    .Select((v, i) =&amp;gt; new Point(i, windowHeight - v))
    .ToArray();   // 将数据转换为一个个的坐标点
g.DrawLines(Pens.Black, points);   // 连接这些点, 画线
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;傅里叶变换:&lt;/h2&gt;
&lt;p&gt;NAudio 中还提供了快速傅里叶变换的方法, 通过傅里叶变换, 可以将时域数据转换为频域数据, 也就是我们所说的频谱&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 我们将对 AverageSamples 进行傅里叶变换, 得到一个复数数组

// 因为对于快速傅里叶变换算法, 需要数据长度为 2 的 n 次方, 这里进行
float log = Math.Ceiling(Math.Log(AverageSamples.Length, 2));   // 取对数并向上取整
int newLen = (int)Math.Pow(2, log);                             // 计算新长度
float[] filledSamples = new float[newLen];
Array.Copy(AverageSamples, filledSamples, AverageSamples.Length);   // 拷贝到新数组
Complex[] complexSrc = filledSamples
    .Select(v =&amp;gt; new Complex(){ X = v })        // 将采样转换为复数
    .ToArray();
FastFourierTransform(false, log, complexSrc);   // 进行傅里叶变换

// 变换之后, complexSrc 则已经被处理过, 其中存储了频域信息
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;分析频域信息:&lt;/h2&gt;
&lt;p&gt;对于傅里叶变换的频域信息, 需要稍加处理才可以方便的使用, 首先是提取有用的信息:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// NAudio 的傅里叶变换结果中, 似乎不存在直流分量(这使我们的处理更加方便了), 但它也是有共轭什么的(也就是数据左右对称, 只有一半是有用的)
// 仍然使用刚刚的 complexSrc 作为变换结果, 它的类型是 Complex[]

Complex[] halfData = complexSrc
    .Take(complexSrc.Length / 2)
    .ToArray();    // 一半的数据
float[] dftData = halfData
    .Select(v =&amp;gt; Math.Sqrt(v.X * v.X + v.Y * v.Y))  // 取复数的模
    .ToArray();    // 将复数结果转换为我们所需要的频率幅度

// 其实, 到这里你完全可以把这些数据绘制到窗口上, 这已经算是频域图象了, 但是对于音乐可视化来讲, 某些频率的数据我们完全不需要
// 例如 10000Hz 的频率, 我们完全没必要去绘制它, 取 最小频率 ~ 2500Hz 足矣
// 对于变换结果, 每两个数据之间所差的频率计算公式为 采样率/采样数, 那么我们要取的个数也可以由 2500 / (采样率 / 采样数) 来得出
int count = 2500 / (cap.WaveFormat.SampleRate / filledSamples.Length);
float[] finalData = dftData.Take(count).ToArray();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;绘制频域图象:&lt;/h2&gt;
&lt;p&gt;得到上面分析后的 finalData 后, 我们就可以直接绘制出来了, 这次使用柔和的曲线绘制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 设定 g 为窗口的 Graphics 对象, height 为窗口高度
PointF[] points = finalData
    .Select((v, i) =&amp;gt; new PointF(i, height - v))
    .ToArray();
g.DrawCurve(Pens.Purple, points);    // Graphics 可以直接绘制曲线
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;更优的绘制:&lt;/h2&gt;
&lt;p&gt;上面的时域和频域图象, 我们都是一股脑的将数据的索引作为 X 坐标, 窗口高度减去数据值作为 Y 坐标, 有两个突出的问题:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据可能无法填满窗口的宽度或者超出窗口的宽度范围&lt;/li&gt;
&lt;li&gt;数据太大时, 也会导致绘制的线条超出窗口高度&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一个问题好解决, 直接使索引所占数据长度的百分比恰好等于 X 坐标相对于窗口宽度的百分比即可:
$$
x = index \div dataLength * windowWidth
$$
对于第二个问题, 有两个解决方案, 一是直接为数据加权重, 例如统一乘 0.5, 使数据减小一节, 二就是套一个函数, 例如 log 函数, 毕竟 log 函数在较高自变量的情况下, 因变量的变化趋势越来越小, 我们只需要对这个 log 函数进行稍加处理, 就可以直接应用到数据变换数据上, 使其不超出窗口绘图区域&lt;/p&gt;
&lt;p&gt;另外, 我们也可以平滑频谱显示(指动画变换), 它的原理大概是这样:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;例如这次进行傅里叶变换的结果是: &lt;code&gt;{0, 100, 50}&lt;/code&gt;,&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下一次傅里叶变换的结果是: &lt;code&gt;{100, 0, 0}&lt;/code&gt;,&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以得出, 增量为: &lt;code&gt;{100, -100, -50}&lt;/code&gt;,&lt;/p&gt;
&lt;p&gt;在更新变换结果时, 我们不再直接将新的结果替换旧的结果, 而是在旧的结果的基础上, 加上增量×权重&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如权重是 &lt;code&gt;0.5&lt;/code&gt; 时, 那么实际增量是: &lt;code&gt;{50, -50, -25}&lt;/code&gt;,&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;那么实际新的值是: &lt;code&gt;{50, 50, 25}&lt;/code&gt;,&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果下一次变换的结果还是 &lt;code&gt;{100, 0, 0}&lt;/code&gt;, 那我们再次从 &lt;code&gt;{50, 50, 25}&lt;/code&gt; 向新值逼近, 权重仍然是 &lt;code&gt;0.5&lt;/code&gt;, 那么实际增量是: &lt;code&gt;{25, -25, -12.5}&lt;/code&gt;,&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意到了吗? 这次的增量是上次增量的一半, 这正好是一个减速运动, 而且新值与旧值的差越大, 变化的就越快, 而它们会不断重合, 因而速度不断变慢, 形成减速运动的频谱图.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;更多内容:&lt;/h2&gt;
&lt;p&gt;更多关于 NAudio 的使用, 可以看这篇文章: &lt;a href=&quot;/posts/20210506194904/&quot;&gt;[C#] NAudio 的各种常见使用方式 播放 录制 转码 音频可视化&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;项目已开源:&lt;/h2&gt;
&lt;p&gt;关于本文章涉及的大部分内容, 均在 &lt;a href=&quot;https://github.com/SlimeNull/AudioTest&quot;&gt;github.com/SlimeNull/AudioTest&lt;/a&gt; 仓库中的 Null.AudioVisualizer 项目中有写. (注释妥当了)&lt;/p&gt;
&lt;p&gt;如果是之后更新过的, 优化了算法的,可以在 &lt;a href=&quot;https://github.com/SlimeNull/AudioVisualizer&quot;&gt;github.com/SlimeNull/AudioVisualizer&lt;/a&gt; 仓库查看源代码. (注释妥当了)&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;其实音频可视化我老早就想做了, 但是本人数学不是非常的好, 不过最后总算是坚持下来了, 弄出来了啊, 心情老激动了&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=&quot;right&quot;&amp;gt;求个赞, 求个评论~&amp;lt;/div&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>[随笔] 音频, 采样, 采样率, 样本大小, 样本编码</title><link>https://slimenull.com/posts/20210506195402/</link><guid isPermaLink="true">https://slimenull.com/posts/20210506195402/</guid><description>音波:首先是初中物理知识, 声音是物体的振动造成的, 所以从时间上看, 这个物体的位置不断变动, 下面这个图象, 就是音波了采样:因为计算机无法存储整个音波, 只能存储音波上一个个的点, 而这一个个的点, 就是采样:采样率:采样率就是采样的密集程度, 如果太过密集, 那么占用的存储空间就会大很多, 但如果采样率小了, 那么这些采样与原波形的形状就有所不同了.样本大小:样本大小表示计算机应该用多大的存储空间来存储一个样本, 有 8bit, 16bit, 32bit, 常用的是 16bit </description><pubDate>Thu, 06 May 2021 19:54:02 GMT</pubDate><content:encoded>&lt;h2&gt;音波:&lt;/h2&gt;
&lt;p&gt;首先是初中物理知识, 声音是物体的振动造成的, 所以从时间上看, 这个物体的位置不断变动, 下面这个图象, 就是音波了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210506195151633.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;采样:&lt;/h2&gt;
&lt;p&gt;因为计算机无法存储整个音波, 只能存储音波上一个个的点, 而这一个个的点, 就是采样:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210506195209949.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;采样率:&lt;/h2&gt;
&lt;p&gt;采样率就是采样的密集程度, 如果太过密集, 那么占用的存储空间就会大很多, 但如果采样率小了, 那么这些采样与原波形的形状就有所不同了.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210506195223293.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;样本大小:&lt;/h2&gt;
&lt;p&gt;样本大小表示计算机应该用多大的存储空间来存储一个样本, 有 8bit, 16bit, 32bit, 常用的是 16bit 和32bit, 样本大小还决定了这个样本能表示的值的范围, 例如 16 bit 的样本只能表示 -32768 到 32767 之间的整数, 如果音频的某个部分超出了这个范围, 那么显然这个采样就不精准了, 如下图, 波峰处就出现了误差.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210506195237718.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;样本编码&lt;/h2&gt;
&lt;p&gt;样本编码指计算机用什么样的格式来存储这个样本, 对于 8bit, 16bit 的样本, 一般是用整数, 对于 32bit 的样本, 则是用浮点数更多.&lt;/p&gt;
</content:encoded></item><item><title>[C#] NAudio 各种常见使用方式 播放 录制 转码 音频可视化</title><link>https://slimenull.com/posts/20210506194904/</link><guid isPermaLink="true">https://slimenull.com/posts/20210506194904/</guid><description>概述在 NAudio 中, 常用类型有 WaveIn, WaveOut, WaveStream, WaveFileWriter, WaveFileReader, AudioFileReader 以及接口: IWaveProvider, ISampleProvider, IWaveIn, IWavePlayerWaveIn 表示波形输入, 继承了 IWaveIn, 例如麦克风输入, 或者计算机正在播放的音频流.WaveOut 表示波形输出, 继承了 IWavePlayer, 用来播放波形音乐, 以 I</description><pubDate>Thu, 06 May 2021 19:49:04 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;在 NAudio 中, 常用类型有 WaveIn, WaveOut, WaveStream, WaveFileWriter, WaveFileReader, AudioFileReader 以及接口: IWaveProvider, ISampleProvider, IWaveIn, IWavePlayer&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;WaveIn 表示波形输入, 继承了 IWaveIn, 例如麦克风输入, 或者计算机正在播放的音频流.&lt;/li&gt;
&lt;li&gt;WaveOut 表示波形输出, 继承了 IWavePlayer, 用来播放波形音乐, 以 IWaveProvider 作为播放源播放音频, 通过拓展方法也支持以 ISampleProvider 作为播放源播放音频&lt;/li&gt;
&lt;li&gt;WaveStream 表示波形流, 它继承了 IWaveProvider, 可以用来作为播放源.&lt;/li&gt;
&lt;li&gt;WaveFileReader 继承了 WaveStream, 用来读取波形文件&lt;/li&gt;
&lt;li&gt;WaveFileWriter 继承了Stream, 用来写入文件, 常用于保存音频录制的数据.&lt;/li&gt;
&lt;li&gt;AudioFileReader 通用的音频文件读取器, 可以读取波形文件, 也可以读取其他类型的音频文件例如 Aiff, MP3&lt;/li&gt;
&lt;li&gt;IWaveProvider 波形提供者, 上面已经提到, 是音频播放的提供者, 通过拓展方法可以转换为 ISampleProvider&lt;/li&gt;
&lt;li&gt;ISampleProvider 采样提供者, 上面已经提到, 通过拓展方法可以作为 WaveOut 的播放源&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;播放音频&lt;/h2&gt;
&lt;p&gt;常用的播放音频方式有两种, 播放波形音乐, 以及播放 MP3 音乐&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;播放波形音乐:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// NAudio 中, 通过 WaveFileReader 来读取波形数据, 在实例化时, 你可以指定文件名或者是输入流, 这意味着你可以读取内存流中的音频数据
// 但是需要注意的是, 不可以读取来自网络流的音频, 因为网络流不可以进行 Seek 操作.

// 此处, 假设 ms 为一个 MemoryStream, 内存有音频数据.
WaveFileReader reader = new WaveFileReader(ms);
WaveOut wout = new WaveOut();
wout.Init(reader);             // 通过 IWaveProvider 为音频输出初始化
wout.Play();                   // 至此, wout 将从指定的 reader 中提供的数据进行播放
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;播放 MP3 音乐:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 播放 MP3 音乐其实与播放波形音乐没有太大区别, 只不过将 WaveFileReader 换成了 Mp3FileReader 罢了
// 另外, 也可以使用通用的 Reader, MediaFoundationReader, 它既可以读取波形音乐, 也可以读取 MP3

// 此处, 假设 ms 为一个 MemoryStream, 内存有音频数据.
Mp3FileReader reader = new Mp3FileReader(ms);
WaveOut wout = new WaveOut();
wout.Init(reader);
wout.Play();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;音频录制&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;录制麦克风输入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 借助 WaveIn 类, 我们可以轻易的捕获麦克风输入, 在每一次录制到数据时, 将数据写入到文件或其他流, 这就实现了保存录音
// 在保存波形文件时需要借助 WaveFileWriter, 当然, 如果你想保存为其他格式, 也可以使用其它的 Writer, 例如 CurWaveFileWriter 以及
// AiffFileWriter, 美中不足的是没有直接写入到 MP3 的 FileWriter
// 需要注意的是, 如果你是用的桌面程序, 那么你可以直接使用 WaveIn, 其回调基于 Windows 消息, 所以无法在控制台应用中使用 WaveIn
// 如果要在控制台应用中实现录音, 只需要使用 WaveInEvent, 它的回调基于事件而不是 Windows 消息, 所以可以通用

WaveIn cap = new WaveIn();   // cap, capture
WaveFileWriter writer = new WaveFileWriter();
cap.DataAvailable += (s, args) =&amp;gt; writer.Write(args.Buffer, 0, args.BytesRecorded);    // 订阅事件
cap.StartRecording();   // 开始录制

// 结束录制时:
cap.StopRecording();    // 停止录制
writer.Close();         // 关闭 FileWriter, 保存数据

// 另外, 除了使用 WaveIn, 你还可以使用 WasapiCapture, 它与 WaveIn 的使用方式是一致的, 可以用来录制麦克风
// Wasapi 全称 Windows Audio Session Application Programming Interface (Windows音频会话应用编程接口)
// 具体 WaveIn, WaveInEvent, WasapiCapture 的性能, 笔者还没有测试过, 但估计不会有太大差异.
// 提示: WasapiCapture 和 WasapiLoopbackCapture 位于 NAudio.Wave 命名空间下
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;录制声卡输出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 录制声卡输出, 也就是录制计算机正在播放的声音, 借助 WasapiLoopbackCapture 即可简单实现, 使用方式与 WasapiCapture 无异

WasapiLoopbackCapture cap = new WasapiLoopbackCapture();
WaveFileWriter writer = new WaveFileWriter();
cap.DataAvailable += (s, args) =&amp;gt; writer.Write(args.Buffer, 0, args.BytesRecorded);
cap.StartRecording();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;高级应用&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;获取计算机实时播放音量大小&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 其实这个是基于刚刚的录制声卡输出的, 录制时的回调中, Buffer, BytesRecorded 指定了此次录制的数据 (缓冲区和数据长度)
// 而这些数据, 其实是计算机对声音的采样(Sample), 具体的采样格式可以查看 WasapiLoopbackCapture 实例的 WaveForamt
// 波形格式中的 Encoding 与 BitsPerSample 是我们所需要的. 一般默认的 Encoding 是 IeeeFloat, 也就是每一个采样都是
// 一个浮点数, 而 BitsPerSample 也就是 32 了. 通过 BitConverter.ToSingle() 我们可以从缓冲区中取得浮点数
// 遍历, 每 32 位一个浮点数, 最终取最大值, 这就是我们所需要的音量了

float volume;
WasapiLoopbackCapture cap = new WasapiLoopbackCapture();
cap.DataAvailable += (s, args) =&amp;gt; volume = Enumerable
                                                .Range(0, args.BytesRecorded / 4)                         // 每一个采样的位置
                                                .Select(i =&amp;gt; BitConverter.ToSingle(args.Buffer, i * 4))   // 获取每一个采样
                                                .Aggregate((v1, v2) =&amp;gt; v1 &amp;gt; v2 ? v1 : v2);                // 找到值最大的采样
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实现音乐可视化
完整版 NAudio 实现音乐可视化: &lt;a href=&quot;/posts/20210509234918/&quot;&gt;[C#] 使用 NAudio 实现音频可视化&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 既然我们已经知道了, 那些数据都是一个个的采样, 自然也可以通过它们来绘制频谱, 只需要进行快速傅里叶变换即可
// 而且有意思的是, NAudio 也为我们准备好了快速傅里叶变换的方法, 位于 NAudio.Dsp 命名空间下
// 提示: 进行傅里叶变换所需要的复数(Complex)类也位于 NAudio.Dsp 命名空间, 它有两个字段, X(实部) 与 Y(虚部)
// 下面给出在 IeeeFloat 格式下的音乐可视化的简单示例:
WasapiLoopbackCapture cap = new WasapiLoopbackCapture();
cap.DataAvailable += (s, args) =&amp;gt;
{
    float[] samples = Enumerable
                          .Range(0, args.BytesRecorded / 4)
                          .Select(i =&amp;gt; BitConverter.ToSingle(args.Buffer, i * 4))
                          .ToArray();   // 获取采样
    
    int log = (int)Math.Ceiling(Math.Log(samples.Length, 2));
    float[] filledSamples = new float[(int)Math.Pow(2, log)];
    Array.Copy(samples, filledSamples, samples.Length);   // 填充数据
    
    int sampleRate = (s as WasapiLoopbackCapture).WaveFormat.SampleRate;    // 获取采样率
    Complex[] complexSrc = filledSamples.Select((v, i) =&amp;gt;
    {
        double deg = i / (double)sampleRate * Math.PI * 2;                  // 获取当前采样率在圆上对应的角度 (弧度制)
        return new Complex()
        {
            X = (float)(Math.Cos(deg) * v),
            Y = (float)(Math.Sin(deg) * v)
        };
    }).ToArray();                                         // 将采样转换为对应的复数 (缠绕到圆)
    
    FastFourierTransform.FFT(false, log, complexSrc);     // 进行傅里叶变换
    double[] result = complexSrc.Select(v =&amp;gt; Math.Sqrt(v.X * v.X + v.Y * v.Y)).ToArray();    // 取得结果
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;音频格式转换&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 对于 Wave, CueWave, Aiff, 这些格式都有其对应的 FileWriter, 我们可以直接调用其 Writer 的 Create***File 来
// 从 IWaveProvider 创建对应格式的文件. 对于 MP3 这类没有 FileWriter 的格式, 可以调用 MediaFoundationEncoder

// 例如一个文件, &quot;./Disconnected.mp3&quot;, 我们要将它转换为 wav 格式, 只需要使用下面的代码, CurWave 与 Aiff 同理
using (Mp3FileReader reader = new Mp3FileReader(&quot;./Disconnected.mp3&quot;))
    WaveFileWriter.CreateWaveFile(&quot;./Disconnected.wav&quot;, reader);

// 从 IWaveProvider 创建 MP3 文件, 假如一个 WaveFileReader 为 src
MediaFoundationEncoder.EncodeToMp3(src, &quot;./NewMp3.mp3&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;提示&lt;/h2&gt;
&lt;p&gt;对于刚刚所说的音频录制, 采样的格式有一点需要注意, 将数据转换为一个 float 数组后, 其中还需要区分音频通道, 例如一般音乐是双通道, WaveFormat 的 Channel 为 2, 那么在 float 数组中, 每两个采样为一组, 一组采样中每一个采样都是一个通道在当前时间内的采样.&lt;/p&gt;
&lt;p&gt;以双通道距离, 下图中, 采样数据中每一个圆圈都表示一个 float 值, 那么每两个采样时间点相同, 而各个通道的采样就是每一组中每一个采样&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/d150eb7b73e62b2aed07a68442f51525.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以对于我们刚刚进行的音乐可视化, 严格意义上来讲, 还需要区分通道&lt;/p&gt;
&lt;h2&gt;示例&lt;/h2&gt;
&lt;p&gt;本文提到的部分内容在 &lt;a href=&quot;https://github.com/SlimeNull/AudioTest&quot;&gt;github.com/SlimeNull/AudioTest&lt;/a&gt; 仓库中有示例, 例如音频可视化, 音频录制, 以及其他零星的示例&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;如有错误, 还请指出&lt;/p&gt;
</content:encoded></item><item><title>[教程] 在 Y 分钟内学会 Python</title><link>https://slimenull.com/posts/20210505184421/</link><guid isPermaLink="true">https://slimenull.com/posts/20210505184421/</guid><description>在 Y 分钟内学会 Python这是翻译, 原文地址: Learn Python in Y Minutes在 90 年代初, Python 由 Guido van Rossum 创造, 现在, 它是最受欢迎的编程语言之一. 因其简明的语法, 我爱上了它. 语法基本上是可以执行的伪代码.提示: 这篇文章适用于 Python 3, 如果你想要学习旧版 Python 2.7, 单击这里# 单行注释以 </description><pubDate>Wed, 05 May 2021 18:44:21 GMT</pubDate><content:encoded>&lt;h1&gt;在 Y 分钟内学会 Python&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;这是翻译, 原文地址: &lt;a href=&quot;https://learnxinyminutes.com/docs/python/&quot;&gt;Learn Python in Y Minutes&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 90 年代初, Python 由 Guido van Rossum 创造, 现在, 它是最受欢迎的编程语言之一. 因其简明的语法, 我爱上了它. 语法基本上是可以执行的伪代码.&lt;/p&gt;
&lt;p&gt;提示: 这篇文章适用于 Python 3, 如果你想要学习旧版 Python 2.7, 单击&lt;a href=&quot;http://learnxinyminutes.com/docs/pythonlegacy/&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 单行注释以 &apos;#&apos; 作为开头

&quot;&quot;&quot;多行注释可以使用三个双引号
   并且经常用与书写文档
&quot;&quot;&quot;

####################################################
## 1. 原始数据类型和操作符
####################################################

# 你可以使用数字
3  # 等同于 3

# 数学运算也是你希望使用的形式
1 + 1  # 结果是 2
8 - 1  # 结果是 7
10 * 2  # 结果是 20
35 / 5  # 结果是 7.0

# 正负数的整除都会向下取整
5 // 3  # 结果是 1
-5 // 3  # 结果是 -2
5.0 // 3.0  # 结果是 1.0 # 在浮点运算中也同样生效
-5.0 // 3.0  # 结果是 -2.0

# 除法的运算结果始终是浮点数
10.0 / 3  # 结果是 3.3333333333333335

# 取模运算
7 % 3  # 结果是 1
# i % j 结果的符号与 j 相同, 这与 C 语言不同
-7 % 3  # 结果是 2

# 幂运算 (x**y, x的y次方)
2 ** 3  # 结果是 8

# 用括号来强制优先运算
1 + 3 * 2  # 结果是 7
(1 + 3) * 2  # 结果是 8

# 布尔值是基本数据类型
True  # 值为 真(true)
False  # 值为 假(false)

# 使用 not 来进行非运算
not True  # 结果是 假
not False  # 结果是 真

# 布尔值操作符:
# 提示, &apos;and&apos; 和 &apos;or&apos; 是区分大小写的
True and False  # 结果是 假
False or True  # 结果是 真

# True 和 False 事实上也等同于 1 和 0, 只不过是使用了不同的关键字
True + True  # 结果是 2
True * 8  # 结果是 8
False - 5  # 结果是 -5

# 比较运算符会检查 True 和 False 的数字值
0 == False  # 结果是 真
1 == True  # 结果是 真
2 == True  # 结果是 假
-5 != False  # 结果是 真

# 对整数使用布尔逻辑操作符, 则会将它们转换为布尔值以求值，但返回未转换的值
# 不要把 bool(ints) 和 位运算 and/or 搞混了
bool(0)  # 返回 假
bool(4)  # 返回 真
bool(-6)  # 返回 真
0 and 2  # 返回 0
-5 or 0  # 返回 -5

# 运算符 &apos;等同于&apos; 是 ==
1 == 1  # 返回 真
2 == 1  # 返回 假

# 运算符 &apos;不等于&apos; 是 !=
1 != 1  # 返回 假
2 != 1  # 返回 真

# 更多比较运算符
1 &amp;lt; 10  # 返回 真
1 &amp;gt; 10  # 返回 假
2 &amp;lt;= 2  # 返回真
2 &amp;gt;= 2  # 返回真

# 检查一个值是否在指定范围内
1 &amp;lt; 2 and 2 &amp;lt; 3  # 返回 真
1 &amp;lt; 3 and 3 &amp;lt; 2  # 返回 假
# 连接起来, 这样看起来会更好看些
1 &amp;lt; 2 &amp;lt; 3  # 返回 真
2 &amp;lt; 3 &amp;lt; 2  # 返回 假

# (is 与 ==) is 将会检查两个变量是否引用了同一个对象, 但是 == 检查
# 两个对象是否指向了相同的值
a = [1, 2, 3, 4]  # 使 a 指向一个新的列表, [1, 2, 3, 4]
b = a  # 使 b 指向 a 所指向的对象
b is a  # 返回 真, a 和 b 引用的是同一个对象
b == a  # 返回 真, a 和 b 的对象是相等的
b = [1, 2, 3, 4]  # 使 b 指向一个新的列表, [1, 2, 3, 4]
b is a  # 返回 假, a 与 b 并不引用同一个对象
b == a  # 返回 真, a 和 b 的对象使相等的

# 字符串可以使用 双引号 &quot; 或 单引号 &apos; 来创建
&quot;这是一个字符串&quot;
&apos;这也是一个字符串&apos;

# 字符串可以相加
&quot;Hello &quot; + &quot;world!&quot;  # 返回 &quot;Hello world!&quot;
&quot;Hello &quot; &quot;world!&quot;  # 等同于 &quot;Hello world!&quot;

# 字符串可以用作一个字符列表
&quot;Hello world!&quot;[0]  # 返回 &apos;H&apos;

# 你可以获取字符串的长度:
len(&quot;这是一个字符串&quot;)  # 返回 7

# 你可以使用 f-字符串 或 格式化字符串 来对字符串文本进行格式化 (在 Python 3.6 或更高版本)
name = &quot;小红&quot;
f&quot;她说她的名字是{name}.&quot;  # 返回 &quot;她说她的名字是小红.&quot;
# 你可以基本的将 Python 表达式放到括号内, 然后它就会被输出到字符串中.
f&quot;{name}是{len(name)}字符长.&quot;  # 返回 &quot;小红是两个字符长&quot;

# None 是一个对象
None  # 返回 None

# 不要使用等同于运算符 &apos;==&apos; 来比较对象和 None
# 使用 &apos;is&apos; 来代替. 这个会检查对象标识是否相同.
&quot;etc&quot; is None  # 返回 假
None is None  # 返回 真

# None, 0, 以及空的字符串/列表/字典/元组, 都计算为 假
bool(0)  # =&amp;gt; 假
bool(&apos;&apos;)  # =&amp;gt; 假
bool([])  # =&amp;gt; 假
bool({})  # =&amp;gt; 假
bool(())  # =&amp;gt; 假

####################################################
## 2. 变量和集合
####################################################

# Python 有一个 print 函数, 用于标准输出
print(&quot;我是Python, 很高兴见到你!&quot;)  # =&amp;gt; 我是Python, 很高兴见到你!

# 默认的, print 函数还会在结尾输出一个换行符
# 使用可选参数 &apos;end&apos; 来改变末尾的字符串.
print(&quot;Hello, world&quot;, end=&quot;!&quot;)  # =&amp;gt; Hello, world!   # 输出后没有换行

# 从控制台获取输入数据的简单方式:
input_string_var = input(&quot;Enter some data:&quot;)  # 以字符串的形式返回输入数据

# Python 中没有变量的声明, 只有赋值.
# 命名规范是使用小写以及下划线, 就像这样: lower_case_with_underscores
some_var = 5
some_var  # =&amp;gt; 5

# 访问一个没有生命的变量是错误的
# 查看控制流来获取更多异常捕捉信息
some_unknown_var  # 这是一个一个未定义的变量, 运行时将抛出 NameError 异常

# if 也可用作一个表达式
# 等同于 C 语言中的 &apos;?:&apos; 三元运算符
&quot;yay&quot; if 0 &amp;gt; 1 else &quot;nay!&quot;  # =&amp;gt; &quot;nay&quot;

# 列表可以存储一个序列, 可以这样定义:
li = []
# 你可以为其指定初始值
other_li = [4, 5, 6]

# 使用 append 方法在列表的末尾添加一些什么东西
li.append(1)  # 现在 li 的值是 [1]
li.append(2)  # 现在 li 的值是 [1, 2]
li.append(4)  # 现在 li 的值是 [1, 2, 4]
li.append(3)  # 现在 li 的值是 [1, 2, 4, 3]
# 使用 pop 方法从列表的末尾移除元素
li.pop()  # 返回 3, 并且现在 li 的值是 [1, 2, 4]
# 重新将它放回去
li.append(3)  # 现在 li 又变成 [1, 2, 4, 3] 了

# 像访问数组一样访问一个列表
li[0]  # =&amp;gt; 1
# 访问列表的最后一个元素
li[-1]  # =&amp;gt; 3

# 如果索引超出界限, 那么会抛出 IndexError 异常
li[4]  # 抛出 IndexError 异常

# 你可以使用切片语法来查看一个范围内的元素
# 起始索引包含在内, 但结束索引不包含在内
# (对于数学类型来讲, 它是一个 闭/开 区间)
li[1:3]  # 返回从索引1到3的一个列表 =&amp;gt; [2, 4]
li[2:]  # 返回从索引2开始的列表 =&amp;gt; [4, 3]
li[:3]  # 返回从开始到索引3的列表 =&amp;gt; [1, 2, 4]
li[::2]  # 返回一个每两个元素选择一个的列表 =&amp;gt; [1, 4]
li[::-1]  # 返回反向排列的列表 =&amp;gt; [3, 4, 2, 1]
# 使用任意组合来实现高级切片
# li[起始:结束:步长]

# 使用切片来创建一个单层的深度拷贝
li2 = li[:]

# 使用 &quot;del&quot; 来删除任意元素
del li[2]  # 现在 li 的值是 [1, 2, 3]

# 删除第一个匹配值的元素
li.remove(2)  # 现在 li 的值是 [1, 3]
li.remove(2)  # 抛出 ValueError 异常, 2 并不在这个列表中

# 在指定索引处插入元素, 列表.insert(索引, 元素)
li.insert(1, 2)  # 现在 li 的值又是 [1, 2, 3] 了

# 获取第一个匹配元素的索引
li.index(2)  # =&amp;gt; 1
li.index(4)  # 抛出 ValueError 异常, 4 不在这个列表中

# 你可以将列表相加
# 提示: values 和 other_li 的值不会被修改
li + other_li  # =&amp;gt; [1, 2, 3, 4, 5, 6]

# 使用 &quot;extend()&quot; 连接列表, extend 的意思是拓展
li.extend(other_li)  # 现在 li 的值是 [1, 2, 3, 4, 5, 6]

# 使用 &quot;in&quot; 检查元素是否存在于列表中
1 in li  # =&amp;gt; True

# 使用 &quot;len()&quot; 检查列表的长度
len(li)  # =&amp;gt; 6

# 元组与列表相像, 但是不可更改
tup = (1, 2, 3)
tup[0]  # =&amp;gt; 1
tup[0] = 3  # 抛出一个 TypeError

# 提示, 长度为一的元组的最后一个元素必须有一个逗号跟随, 但是
# 其他长度的元组, 尽管是0, 也不需要
type((1))  # =&amp;gt; &amp;lt;class &apos;int&apos;&amp;gt;
type((1,))  # =&amp;gt; &amp;lt;class &apos;tuple&apos;&amp;gt;
type(())  # =&amp;gt; &amp;lt;class &apos;tuple&apos;&amp;gt;

# 大多数的列表操作符都可以在元组上使用
len(tup)  # =&amp;gt; 3
tup + (4, 5, 6)  # =&amp;gt; (1, 2, 3, 4, 5, 6)
tup[:2]  # =&amp;gt; (1, 2)
2 in tup  # =&amp;gt; True

# 你可以将元组(或者列表)解包为变量
a, b, c = (1, 2, 3)  # 现在, a 是 1, b 是 2, c 是 3
# 你还可以使用拓展解包
a, *b, c = (1, 2, 3, 4)  # 现在, a 是 1, b 是 [2, 3], c 是 4
# 默认情况下, 即便你忽略括号, 也会创建一个元组
d, e, f = 4, 5, 6  # 元组 4, 5, 6 被解包为变量 d, e, f
# 变量值分别如下: d = 4, e = 5 以及 f = 6
# 那么看看交换两个值是多么的简单
e, d = d, e

# 字典用于存储从键到值的映射
empty_dict = {}  # 创建了一个空字典
# 下面是一个带有初始值的字典
filled_dict = {&quot;one&quot;: 1, &quot;two&quot;: 2, &quot;three&quot;: 3}

# 提示: 字典的键必须是不可变的类型.  这是为了确保
# 键能够转换为用于快速查询的常量哈希值.
# 不可变类型包含整数, 浮点数, 字符串, 元组
invalid_dict = {[1, 2, 3]: &quot;123&quot;}  # 抛出 TypeError: unhashable type: &apos;list&apos; 异常. (类型错误: 无法进行哈希化的类型: &apos;列表&apos;)
valid_dict = {(1, 2, 3): [1, 2, 3]}  # 然而, 值可以是任何类型

# 使用 [] 来查询值
filled_dict[&quot;one&quot;]  # =&amp;gt; 1

# 使用 &quot;keys()&quot; 来获取所有的键并作为一个可迭代对象返回. 我们需要在 list() 中将调用结果转换
# 为一个列表. 这个稍后再讲. 提示 - 在 Python 低于 3.7 的版本中
# 字典键索引的顺序是无法保证的. 你的结果可能不与下面的例子完全相等. 然而, 在 Python 3.7 中, 字典
# 元素会保持它们被插入到字典的顺序
list(filled_dict.keys())  # =&amp;gt; [&quot;three&quot;, &quot;two&quot;, &quot;one&quot;] 在低于 3.7 的 Python 版本中
list(filled_dict.keys())  # =&amp;gt; [&quot;one&quot;, &quot;two&quot;, &quot;three&quot;] 在 3.7 或更高的 Python 版本中

# 使用 &quot;values()&quot; 来获取所有的值并作为可迭代对象返回. 同样, 我们需要将其在
# list() 中转换, 以取出这个可迭代对象的值. 提示 - 和上面键的顺序是一样的
list(filled_dict.values())  # =&amp;gt; [3, 2, 1] 在低于 3.7 的 Python 版本中
list(filled_dict.values())  # =&amp;gt; [1, 2, 3] 在 3.7 或更高的 Python 版本中

# 使用 &quot;in&quot; 来检查键是否在字典中
&quot;one&quot; in filled_dict  # =&amp;gt; True
1 in filled_dict  # =&amp;gt; False

# 通过不存在的键来查找字典, 会抛出 KeyError 异常
filled_dict[&quot;four&quot;]  # KeyError

# 使用 &quot;get()&quot; 方法来避免 KeyError 异常
filled_dict.get(&quot;one&quot;)  # =&amp;gt; 1
filled_dict.get(&quot;four&quot;)  # =&amp;gt; None
# 这个方法支持当找不到值时返回指定的默认值
filled_dict.get(&quot;one&quot;, 4)  # =&amp;gt; 1
filled_dict.get(&quot;four&quot;, 4)  # =&amp;gt; 4

# &quot;setdefault()&quot; 只有在给定键不存在的时候将值插入到字典
filled_dict.setdefault(&quot;five&quot;, 5)  # filled_dict[&quot;five&quot;] 被设置为了 5
filled_dict.setdefault(&quot;five&quot;, 6)  # filled_dict[&quot;five&quot;] 仍然是 5

# 向字典中添加内容
filled_dict.update({&quot;four&quot;: 4})  # =&amp;gt; {&quot;one&quot;: 1, &quot;two&quot;: 2, &quot;three&quot;: 3, &quot;four&quot;: 4}
filled_dict[&quot;four&quot;] = 4  # 向字典中添加的另一种方式

# 自 Python 3.5 以来, 你还可以使用拓展解包选项
{&apos;a&apos;: 1, **{&apos;b&apos;: 2}}  # =&amp;gt; {&apos;a&apos;: 1, &apos;b&apos;: 2}
{&apos;a&apos;: 1, **{&apos;a&apos;: 2}}  # =&amp;gt; {&apos;a&apos;: 2}

# 集合用来存储 ... 额, 集合
empty_set = set()  # 空集合
# 用一组值来初始化一个集合, 嗯, 看起来有点像字典, 抱歉
some_set = {1, 1, 2, 2, 3, 4}  # some_set 现在的值是 {1, 2, 3, 4}

# 与字典中的键相似, 集合的元素必须是不可变的
invalid_set = {[1], 1}  # =&amp;gt; 抛出一个 TypeError: unhashable type: &apos;list&apos; (类型错误: 无法进行哈希化的类型: &apos;列表&apos;)
valid_set = {(1,), 1}

# 向集合中添加一个或多个条目
filled_set = some_set
filled_set.add(5)  # filled_set 现在是 {1, 2, 3, 4, 5}
# 集合是不包含重复元素的
filled_set.add(5)  # 还是与之前一样 {1, 2, 3, 4, 5}

# 使用 &amp;amp; 取交集
other_set = {3, 4, 5, 6}
filled_set &amp;amp; other_set  # =&amp;gt; {3, 4, 5}

# 使用 | 取并集
filled_set | other_set  # =&amp;gt; {1, 2, 3, 4, 5, 6}

# 使用 - 取差集
{1, 2, 3, 4} - {2, 3, 5}  # =&amp;gt; {1, 4}

# 使用 ^ 取对称差集
{1, 2, 3, 4} - {2, 3, 5}  # =&amp;gt; {1, 4, 5}

# 检查左侧的集合是否是右侧集合的超集
{1, 2} &amp;gt;= {1, 2, 3}  # =&amp;gt; False

# 检查左侧的集合是否是右侧集合的子集
{1, 2} &amp;lt;= {1, 2, 3}  # =&amp;gt; True

# 使用 in 来检查是否存在于集合中
2 in filled_set  # =&amp;gt; True
10 in filled_set  # =&amp;gt; False

# 生成一个单层的深层副本
filled_set = some_set.copy()  # filled_set 是 {1, 2, 3, 4, 5}
filled_set is some_set  # =&amp;gt; False

####################################################
## 3. 控制流和可迭代对象
####################################################

# 首先我们声明一个变量
some_var = 5

# 这是一个 if 语句, 缩进在 Python 中非常重要
# 约定语法是使用四个空格, 而不是水平制表符(tab)
# 这个将会打印 &quot;some_var 比 10 小&quot;
if some_var &amp;gt; 10:
    print(&quot;some_var 比 10 大&quot;)
elif some_var &amp;lt; 10:  # 这个 elif 语句是可选的
    print(&quot;some_var 比 10 小&quot;)
else:  # 这个也是可选的
    print(&quot;some_var 与 10 相等&quot;)

&quot;&quot;&quot;
for 语句用来循环遍历列表
将会打印:
    狗是哺乳动物
    猫是哺乳动物
    老鼠是哺乳动物
&quot;&quot;&quot;
for animal in [&quot;狗&quot;, &quot;猫&quot;, &quot;老鼠&quot;]:
    # 你可以使用 format() 来插入格式化字符串
    print(&quot;{}是哺乳动物&quot;.format(animal))

&quot;&quot;&quot;
&quot;range(数字)&quot; 返回一个数字的可迭代对象
从0到给定数字
将会打印:
    0
    1
    2
    3
&quot;&quot;&quot;
for i in range(4):
    print(i)

&quot;&quot;&quot;
&quot;range(较小的数, 较大的数)&quot; 返回一个数字的可迭代对象
从较小的数字到较大的数字
将会打印:
    4
    5
    6
    7
&quot;&quot;&quot;
for i in range(4, 8):
    print(i)

&quot;&quot;&quot;
&quot;range(较小的数, 较大的数, 步长)&quot; 返回一个数字的可迭代对象
从较小的数到较大的数, 以步长未每次增长的值
如果步长没有指定, 默认值则是 1
将会打印:
    4
    6
&quot;&quot;&quot;
for i in range(4, 8, 2):
    print(i)

&quot;&quot;&quot;
循环一个列表, 并且同时检索列表中每一个条目的索引和值
将会打印:
    0 狗
    1 猫
    2 老鼠
&quot;&quot;&quot;
animals = [&quot;狗&quot;, &quot;猫&quot;, &quot;老鼠&quot;]
for i, value in enumerate(animals):
    print(i, value)

&quot;&quot;&quot;
while 循环将一直进行到条件不再满足为止
将会打印:
    0
    1
    2
    3
&quot;&quot;&quot;
x = 0
while x &amp;lt; 4:
    print(x)
    x += 1  # x = x + 1 的简写

# 使用 try/except 语句块来处理异常
try:
    # 使用 &quot;raise&quot; 来抛出异常
    raise IndexError(&quot;这是一个索引错误&quot;)
except IndexError as e:
    pass  # pass 只是一个占位符(不进行任何操作). 通常你需要在这里对异常进行处理
except (TypeError, NameError):
    pass  # 如果需要, 你可以同时处理多个异常.
else:  # try/except 语句块的可选语句. 必须在所有 except 语句块的后面
    print(&quot;一切正常!&quot;)  # 仅在 try 语句内没有任何异常时运行
finally:  # 在任何情况下都会执行
    print(&quot;我们可以在这里进行资源清理&quot;)

# 你可以使用 with 语句代替 try/finally 来清理资源
with open(&quot;我的文件.txt&quot;) as f:
    for line in f:
        print(line)

# 向文件中写入内容
contents = {&quot;aa&quot;: 12, &quot;bb&quot;: 21}
with open(&quot;我的文件1.txt&quot;, &quot;w+&quot;) as file:
    file.write(str(content))  # 向文件中写入字符串

with open(&quot;我的文件2.txt&quot;, &quot;w+&quot;) as file:
    file.write(json.dumps(content))  # 向文件中写入一个对象

# 从文件中读取
with open(&quot;我的文件1.txt&quot;, &quot;r+&quot;) as file:
    contents = file.read()  # 从文件中读取一个字符串
print(contents)
# 打印: {&quot;aa&quot;, 12, &quot;bb&quot;: 21}

with open(&quot;我的文件2.txt&quot;, &quot;r+&quot;) as file:
    contents = json.load(file)  # 从文件中读取一个json对象
print(contents)
# print: {&quot;aa&quot;: 12, &quot;bb&quot;: 21}


# Python 提供了一个叫做 Iterable(可迭代的) 的基本抽象
# 一个可迭代对象是一个可视为序列的对象
# range 函数返回的对象就是一个可迭代对象

filled_dict = {&quot;one&quot;: 1, &quot;two&quot;: 2, &quot;three&quot;: 3}
our_iterable = filled_dict.keys()
print(our_iterable)  # =&amp;gt; dict_keys([&apos;one&apos;, &apos;two&apos;, &apos;three&apos;]). 这是一个实现了 Iterable 接口的对象

# 我们可以检索它
for i in our_iterable:
    print(i)  # 打印 one, two, three

# 然而, 我们不可以通过索引来找到元素
our_iterable[1]  # 抛出 TypeError 异常

# 一个可迭代对象即为能够创建迭代器的对象
our_iterator = iter(our_iterable)

# 迭代器是一个能够记住当前通过它迭代状态的对象
# 我们可以通过 &quot;next()&quot; 来获取下一个对象
next(our_iterator)  # =&amp;gt; &quot;one&quot;

# 它会保持我们遍历的状态
next(our_iterator)  # =&amp;gt; &quot;two&quot;
next(our_iterator)  # =&amp;gt; &quot;three&quot;

# 在迭代器已经返回完所有的数据后, 将会抛出 StopIteration 异常
next(our_iterator)  # 抛出 StopIteration 异常

# 我们也可以检索它, 事实上, &quot;for&quot; 语句就是隐式的执行了这个操作
our_iterator = iter(our_iterable)
for i in iterator:
    print(i)

# 你可以通过调用 list() 来获取可迭代对象或迭代器的所有元素.
list(our_iterable)  # =&amp;gt; 返回 [&quot;one&quot;, &quot;two&quot;, &quot;three&quot;]
list(our_iterator)  # =&amp;gt; 返回 [] 因为迭代状态已经被保存下来


####################################################
## 4. 函数
####################################################

# 使用 &quot;def&quot; 来创建一个新的函数
def add(x, y):
    print(&quot;x 是 {} 以及 y is {}&quot;.format(x, y))
    return x + y


# 调用带参数的函数
add(5, 6)  # =&amp;gt; 输出 &quot;x 是 5 以及 y 是 6&quot;, 并返回 11

# 调用函数的另一种方式是带有关键字参数
add(y=6, x=5)  # 关键字参数可以在任何顺序下正常运行


# 你可以定义接受数量可变的位置参数的函数
def varargs(*args):
    return args


varargs(1, 2, 3)  # =&amp;gt; (1, 2, 3)


# 同样, 你可以定义接受数量可变的关键字参数的函数
def keyword_args(**kwargs):
    return kwargs


# 来调用一下, 然后看看会发生什么
keyword_args(big=&quot;foot&quot;, loch=&quot;ness&quot;)  # =&amp;gt; {&quot;big&quot;: &quot;foot&quot;, &quot;loch&quot;: &quot;ness&quot;}


# 只要你想, 你也可以同时使用它们两个
def all_the_args(*args, **kwargs):
    print(args)
    print(kwargs)


&quot;&quot;&quot;
all_the_args(1, 2, a=3, b=4) 将会打印:
    (1, 2)
    {&quot;a&quot;: 3, &quot;b&quot;: 4}
&quot;&quot;&quot;

# 在调用函数时, 你可以做相反的 args/kwargs!
# 使用 * 来拓展元组, 以及使用 ** 来拓展 kwargs.
args = (1, 2, 3, 4)
kwargs = {&quot;a&quot;: 3, &quot;b&quot;: 4}
all_the_args(*args)  # 等同于 all_the_args(1, 2, 3, 4)
all_the_args(**kwargs)  # 等同于 all_the_args(a=3, b=4)
all_the_args(*args, **kwargs)  # 等同于 all_the_args(1, 2, 3, 4, a=3, b=4)


# 返回多个值(通过赋值元组)
def swap(x, y):
    return y, x  # 通过没有括号的元组来返回多个值
    # (提示: 括号虽然没有写, 但是也可以添加上)


x = 1
y = 2
x, y = swap(x, y)  # =&amp;gt; x = 2, y = 1
# (x, y) = swap(x,y)  # 同样, 括号虽没有写, 但是也可以添加上

# 函数作用域
x = 5


def set_x(num):
    # 局部变量 x 与全局变量 x 是不同的
    x = num  # =&amp;gt; 43
    print(x)  # =&amp;gt; 43


def set_global_x(num):
    global x
    print(x)  # =&amp;gt; 5
    x = num  # =&amp;gt; 全局变量 x 现在被设置为 6 了
    print(x)  # =&amp;gt; 6


set_x(43)
set_global_x(6)


# Python 支持本地函数
def create_adder(x):
    def adder(y):
        return x + y

    return adder


add_10 = create_adder(10)
add_10(3)  # =&amp;gt; 13

# 也支持匿名函数
(lambda x: x &amp;gt; 2)(3)  # =&amp;gt; True
(lambda x, y: x ** 2 + y ** 2)(2, 1)  # =&amp;gt; 5

# 下面是一些内置的高阶函数
list(map(add_10, [1, 2, 3]))  # =&amp;gt; [11, 12, 13]
list(map(max, [1, 2, 3], [4, 2, 1]))  # =&amp;gt; [4, 2, 3]

list(filter(lambda x: x &amp;gt; 5, [3, 4, 5, 6, 7]))  # =&amp;gt; [6, 7]

# 你可以使用列表推导式来实现优秀的映射与过滤
# 列表推导式存储可嵌套的列表以输出
[add_10(i) for i in [1, 2, 3]]  # =&amp;gt; [11, 12, 13]
[x for x in [3, 4, 5, 6, 7] if x &amp;gt; 5]  # =&amp;gt; [6, 7]

# 同样, 你可以构建集合和字典推导式
{x for x in &apos;abcddeef&apos; if x not in &apos;abc&apos;}  # =&amp;gt; {&apos;d&apos;, &apos;e&apos;, &apos;f&apos;}
{x: x ** 2 for x in range(5)}  # =&amp;gt; {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

####################################################
## 5. 模块
####################################################

# 导入模块, 调用模块中的函数
import math

print(math.sqrt(16))  # =&amp;gt; 返回4.0

# 你可以从模块中获取指定的函数
from math import ceil, floor

print(ceil(3.7))  # =&amp;gt; 4.0
print(floor(4.7))  # =&amp;gt; 3.0

# 你可以从模块中导入所有函数
# 警告: 不推荐这么做
from math import *

# 你可以在导入模块时, 为其起一个别名
import math as m

math.sqrt(16) == m.sqrt(16)  # =&amp;gt; True

# Python 模块只是普通的 Python 文件. 你
# 可以编写你自己的模块, 然后导入它们. 模块
# 的名字与文件名是相同的

# 你可以查找模块内定义的函数和属性
import math

dir(math)


# 如果有一个名为 math.py 的 Python 脚本与
# 当前脚本在同一个目录下, 那么这个文件将会
# 代替 Python 内建模块以加载.
# 这是因为本地目录有比 Python 的内建库更高
# 的优先级.


####################################################
## 6. 类
####################################################

# 使用 &quot;class&quot; 语句来创建一个类
class Human:
    # 类的属性. 将在类的所有实例中共享
    species = &quot;SlimeNull&quot;

    # 基本的初始化器, 这个将会在类实例化时调用
    # 提示, 双前置和后置下划线表示 Python 使用
    # 的对象或属性, 但是它们存在于用户控制的命名
    # 空间. 方法(或者是对象, 属性) 像 __init__, __str__,
    # __repr__ 等等, 被称为特殊方法(有时也成为魔法方法)
    # 你不应该创建你自己的, 与它们重名的成员
    def __init__(self, name):
        # 将参数赋值给实例的 name 属性
        self.name = name

        # 初始化属性
        self._age = 0

    # 实例的方法. 所有的方法都需要使用 &quot;self&quot; 作为第一个参数
    def say(self, msg):
        print(&quot;{name}: {message}&quot;.format(name=self.name, message=msg))

        # 实例的另一个方法
        def sing(self):
            return &apos;哟... 哟... 麦克风检查... one... one two...&apos;

    # 类的方法, 所有的实例都能够访问
    # 它们被调用时, 调用它的类将作为调用的第一个参数
    @classmethod
    def get_species(cls):
        return cls.species;

    # 静态方法在调用时, 不会有类或实例的引用传入
    @staticmethod
    def grunt():
        return &quot;*咕噜咕噜*&quot;

    # 属性就像一个获取器(getter)
    # 它将这个方法 age() 作为一个同名的, 只读的属性返回
    # 但是, 在 Python 中不需要写繁琐的获取器和设置器(setter)
    @property
    def age(self):
        return self._age

    # 这个将允许属性被设置
    @age.setter
    def age(self, age):
        self._age = age

    # 这个将允许属性被删除
    @age.deleter
    def age(self):
        del self._age


# 当 Python 解释器读取源文件并执行时
# __name__ 会确保这个代码块在模块中
# 是作为主程序执行的
if __name__ == &apos;__main__&apos;:
    # 实例化类
    i = Human(name=&quot;小明&quot;)
    i.say(&quot;嗨~&quot;)
    j = Human(&quot;小红&quot;)
    j.say(&quot;你好哇~&quot;)
    # i 和 j 是类型 Human 的实例, 换句话说, 他们都是 Human 对象

    # 调用类的方法
    i.say(i.get_species())          # =&amp;gt; &quot;小明: SlimeNull&quot;
    # 更改共享属性
    Human.species = &quot;Little Wu♂ Fairy&quot;
    i.say(i.get_species())          # =&amp;gt; &quot;小明: Little Wu♂ Fairy&quot;
    j.say(j.get_species())          # =&amp;gt; &quot;小红: Little Wu♂ Fairy&quot;

    # 调用静态方法
    print(Human.grunt())            # =&amp;gt; &quot;*咕噜咕噜*&quot;

    # 静态方法也可以通过实例来调用
    print(i.grunt())                # =&amp;gt; &quot;*咕噜咕噜*&quot;

    # 为这个实例更新属性
    i.age = 42
    # 获取属性
    i.say(i.age)                    # =&amp;gt; &quot;小明: 42&quot;
    j.say(j.age)                    # =&amp;gt; &quot;小红: 0&quot;
    # 删除属性
    del i.age
    # i.age                         # =&amp;gt; 将会抛出 AttributeError 异常


####################################################
## 6.1 继承
####################################################

# 继承允许定义的新的子类从父类继承
# 方法与变量

# 使用上面定义的 Human 类作为基类(父类), 我们可以定义
# 一个子类, Superhero, 它将继承 Human 类的变量, 例如
# &quot;species&quot;, &quot;name&quot;, 以及 &quot;age&quot;, 方法也是如此, 例如
# &quot;sing&quot; 与 &quot;grunt&quot;. 但同时它也可以有自己的特殊属性.

# 你可以将上面的类存储到它们自己的文件中来采用模块化文件的优点
# 命名为, human.py

# 从别的文件中导入函数, 需要使用下面的格式
# from &quot;没有后缀的文件名&quot; import &quot;函数或者类&quot;

from human import Human


# 在类型定义处以参数的形式指定父类
class Superhero(Human):

    # 如果字类仅仅是从父类继承所有定义, 且没有
    # 任何更改, 你可以只在这里写一个 &quot;pass&quot; 关键字 (别的不需要写)
    # 但在我们这种情况下, pass 就要被注释掉以为 Superhero 类创建
    # 它特有的内容
    # pass

    # 字类可以覆盖父类的属性
    species = &quot;Superhuman&quot;

    # 字类会自动的从父类继承构造函数, 包括
    # 它的参数, 但是也可以定义另外的参数, 或者定义
    # 然后覆盖它的方法, 例如这个类的构造函数
    # 这个构造函数从 Human 类继承了 &quot;name&quot; 参数并且
    # 添加了 &quot;superpower&quot; 和 &quot;movie&quot; 参数
    def __init__(self, name, movie=False,
                 superpowers = [&quot;力大无穷&quot;, &quot;金刚不坏之身&quot;]):

        # 添加额外的类型属性
        self.fictional = True
        self.movie = movie
        # 注意可变的默认值, 因为它们默认是共享的
        self.superpowers = superpowers

        # &quot;super&quot; 函数使你可以访问被字类重写了的
        # 父类的方法, 在这里, 我们要使用 &quot;__init__&quot;
        # 这将会调用父类的构造函数
        super().__init__(name)

    # 覆盖 sing 方法
    def sing(self):
        return &quot;Dun, dun, DUN!&quot;

    # 添加附加的实例方法
    def boast(self):
        for power in self.superpowers:
            print(&quot;我有{pow}的能力!&quot;.format(pow=power))


if __name__ == &apos;__main__&apos;:
    sup = Superhero(name=&quot;蒂克&quot;)

    # 实例类型检查
    if isinstance(sup, Human):
        print(&quot;我是人类&quot;)
    if type(sup) is Superhero:
        print(&quot;我是一个超级英雄&quot;)

    # 通过使用 getattr() 和 super() 来获取方法解析搜索顺序
    # 这个属性是动态的, 并且可以被更新
    print(Superhero.__mro__)    # =&amp;gt; (&amp;lt;class &apos;__main__.Superhero&apos;&amp;gt;,
                                # =&amp;gt; &amp;lt;class &apos;human.Human&apos;&amp;gt;, &amp;lt;class &apos;object&apos;&amp;gt;)

    # 通过它自己的类型属性来调用父类的方法
    print(sup.get_species())    # =&amp;gt; Superhuman

    # 调用被重写了的方法
    print(sup.sing())           # =&amp;gt; Dun, dun, DUN!

    # 调用 Human 的方法
    sup.say(&quot;勺子?&quot;)             # =&amp;gt; 蒂克: 勺子?

    # 调用仅存在于 Superhero 中的方法
    sup.boast()                 # =&amp;gt; 我有力大无穷的能力!
                                # =&amp;gt; 我有金刚不坏之身的能力!

    # 继承的类型属性
    sup.age = 32
    print(sup.age)              # =&amp;gt; 32

    # 仅在 Superhero 中存在的属性
    print(&apos;我能获得奥斯卡奖吗?&apos; + str(sup.movie))

####################################################
## 6.2 多重继承
####################################################

# 另一个类的定义
# bat.py
class Bat:

    species = &apos;贝蒂&apos;

    def __init__(self, can_fly=True):
        self.fly = can_fly

    # 这个类也有一个说的方法
    def say(self, msg):
        msg = &apos;... ... ...&apos;
        return msg

    # 并且它也有自己的方法
    def sonar(self):
        return &apos;))) ... (((&apos;

if __name__ == &apos;__main__&apos;:
    b = Bat()
    print(b.say(&apos;你好&apos;))
    print(b.fly)


# 然后现在另一个类型定义继承自 Superhero 和 Bat
# superhero.py
from superhero import Superhero
from bat import Bat

# 定义 Batman 并同时继承 Superhero 和 Bat
class Batman(Superhero, Bat):

    def __init__(self, *args, **kwargs):
        # 继承属性通常需要 super
        # super(Batman, self).__init__(*args, **kwargs)
        # 然而在这里处理多继承, 所以 super()
        # 仅仅适用于 MRO 列表的下一个基类
        # 所以, 我们需要为所有父类显式的调用 __init__
        # *args 和 **kwargs 允许使用非常简洁的方式传递所有参数,
        # 每一个父类都 &quot;给洋葱剥一层皮&quot;
        Superhero.__init__(self, &apos;anonymous&apos;, movie=True,
                           superpowers=[&apos;Wealthy&apos;], *args, **kwargs)
        Bat.__init__(self, *args, can_fly=False, **kwargs)
        # 覆盖相同名称的属性的值
        self.name = &quot;悲伤的阿弗莱克&quot;

    def sing(self):
        return &quot;呐 呐 呐 呐 呐 蝙蝠侠!&quot;


if __name__ == &apos;__main__&apos;:
    sup = Batman()

    # 通过使用 getattr() 和 super() 来获取方法解析搜索顺序
    # 这个属性是动态的, 并且可以被更新
    print(Batman.__mro__)       # =&amp;gt; (&amp;lt;class &apos;__main__.Batman&apos;&amp;gt;,
                                # =&amp;gt; &amp;lt;class &apos;superhero.Superhero&apos;&amp;gt;,
                                # =&amp;gt; &amp;lt;class &apos;human.Human&apos;&amp;gt;,
                                # =&amp;gt; &amp;lt;class &apos;bat.Bat&apos;&amp;gt;, &amp;lt;class &apos;object&apos;&amp;gt;)

    # 通过它自己的类型属性来调用父类的方法
    print(sup.get_species())    # =&amp;gt; Superhuman

    # 调用被重写了的方法
    print(sup.sing())           # =&amp;gt; 呐 呐 呐 呐 呐 蝙蝠侠!

    # 调用 Human 的方法, 原因是继承问题
    sup.say(&apos;我同意&apos;)            # =&amp;gt; 悲伤的阿弗莱克: 我同意

    # 调用存在于第二个祖先的方法
    print(sup.sonar())          # =&amp;gt; ))) ... (((

    # 继承了的类型属性
    sup.age = 100
    print(sup.age)              # =&amp;gt; 100

    # 从第二个祖先继承的默认值被重写了的属性
    print(&apos;我能飞吗?&apos; + str(sup.fly))  # =&amp;gt; 我能飞吗? 不能



####################################################
## 7. 高级
####################################################

# 生成器可以帮助你写一些更简便的代码 (偷懒)
def double_numbers(iterable):
    for i in iterable:
        yield i + i

# 生成器是 高效内存 的, 因为它们只加载需要的数据
# 处理可迭代对象的下一个值, 这使他们可以在非常大
# 的值域上执行操作.
# 提示: `range` 在 Python3 中取代了 `xrange`
for i in double_numbers(range(1, 900000000)):  # `range` 是一个生成器
    print(i)
    if (i &amp;gt; 30):
        break

# 就像你能创建列表推导式一样, 你也可以创建
# 生成器推导式
values = (-x for x in [1,2,3,4,5])
for x in values:
    print(x)  # 打印 -1 -2 -3 -4 -5 到 控制台/终端

# 你也可以将生成器推导式转换为一个列表
values = (-x for x in [1,2,3,4,5])
gen_to_list = list(values)
print(gen_to_list)  # =&amp;gt; [-1, -2, -3, -4, -5]

# 装饰器
# 在这个例子中, `beg` 与 `say` 交换. 如果 say_please 是 True, 那么它
# 将会改变返回的消息
from functools import wraps


def beg(target_function):
    @wraps(target_function)
    def wrapper(*args, **kwargs):
        msg, say_please = target_function(*args, **kwargs)
        if say_please:
            return &quot;{} {}&quot;.format(msg, &quot;求你了, 我很穷 :(&quot;)
        return msg

    return wrapper


@beg
def say(say_please=False):
    msg = &quot;能为我买瓶啤酒吗?&quot;
    return msg, say_please


print(say())                 # 能为我买瓶啤酒吗?
print(say(say_please=True))  # 能为我买瓶啤酒吗? 求你了, 我很穷 :(
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C#] C# 与 Java 的一些差异</title><link>https://slimenull.com/posts/20210418110746/</link><guid isPermaLink="true">https://slimenull.com/posts/20210418110746/</guid><description>这篇文章, 可以帮助你借助 C# 的知识快速入门 Java, 并且详细介绍 C# 与 Java 的重要差异1. 数据类型在 C# 中, 基本的数据类型都有别名, 例如字符串 String, 可以简写为 string, Int32 可以简写为 int, 但是在 Java 中, 不存在这些. 使用字符串, 必须要首字母大写, 使用布尔值必须要用 boolean.在 C# 中, String 数据基本数据类型, 而在 Java 中, 严格来讲, 它不属于基本数据类型. Java 中的基本数据类型更像是.</description><pubDate>Sun, 18 Apr 2021 11:07:46 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;这篇文章, 可以帮助你借助 C# 的知识快速入门 Java, 并且详细介绍 C# 与 Java 的重要差异&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. 数据类型&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在 C# 中, 基本的数据类型都有别名, 例如字符串 String, 可以简写为 string, Int32 可以简写为 int, 但是在 Java 中, 不存在这些. 使用字符串, 必须要首字母大写, 使用布尔值必须要用 boolean.&lt;/li&gt;
&lt;li&gt;在 C# 中, String 数据基本数据类型, 而在 Java 中, 严格来讲, 它不属于基本数据类型. Java 中的基本数据类型更像是 C# 中的值类型, 只有 int, char, boolean 这些直接存储的数据类型才是基本数据类型.&lt;pre&gt;&lt;code&gt;String str = &quot;这是一个字符串&quot;;          // 正确
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;在 C# 中, int 就是 int, 它与其他复杂的数据类型例如 StringBuilder 差异并不大, 但是在 Java 中, int, char, boolean 这类基本数据类型不可以作为泛型参数来使用, 例如你不能创建一个存储 int 的 List, 如果要创建存储 int 的 List, 你需要使用 int 的包装类 Integer, 创建 ArrayList&amp;lt;Integer&amp;gt;.&lt;pre&gt;&lt;code&gt; var v = new ArrayList&amp;lt;int&amp;gt;();        // 错误
 var v = new ArrayList&amp;lt;Integer&amp;gt;();    // 正确
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Java 不存在值类型, 只有基本数据类型和引用对象类型, 两者用法不同.&lt;/li&gt;
&lt;li&gt;Java 的基本数据类型不存在成员, 例如在 C# 中, int 有静态常量 MaxValue, 但是 Java 中的 int 没有, 如果要获取整数的最大值, 需要使用 int 的包装类 Integer&lt;pre&gt;&lt;code&gt;int v = int.MAX_VALUE;      // 错误
int v = Integer.MAX_VALUE;  // 正确
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2. 枚举&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在 C# 中, 枚举是特殊的值类型, 本质是 int, 定义起来非常简单, 在 Java 中, 如果要自定义每一个枚举成员所对应的 int 值, 则定义起来非常繁琐:&lt;pre&gt;&lt;code&gt;public enum ColumnAlignment {
    Left(1), Right(2);

    private int value;
    private ColumnAlignment(int value) {
        this.value = value;
    }
    public int value(){
        return this.value;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. Linq 与 Stream&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在 C# 中, 对于可迭代对象的批量处理, 我们经常使用 Linq, 但是在 Java 中, 是不支持拓展函数的, 所以自然无法实现 Linq 这类方便的操作类, Stream 勉强可以作为 Linq 的替代品, 例如与 Linq 中 Select 方法对应的 Stream.map:&lt;pre&gt;&lt;code&gt;// C#:
List&amp;lt;string&amp;gt; strs = new int[]{1,2,3,4,5,6,7,8,9,0}.Select(v =&amp;gt; v.ToString()).ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Java:
List&amp;lt;String&amp;gt; strs = Arrays.stream(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}).map(v -&amp;gt; v.toString()).collect(Collectors.toList());
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不过在实际使用中, 由于 Java 区分 int 和 Integer, 所以会有诸多不便, 但大多都有解决方案, 所以 Java 只是会让人感到有些抓狂罢了.&lt;/p&gt;
&lt;h2&gt;4. Lambda:&lt;/h2&gt;
&lt;p&gt;匿名函数, 这绝对是最舒服的东西之一了, 在使用 Linq 的时候肯定无时无刻使用 lambda 的.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 C# 中, lambda 表达式的操作符是: &lt;code&gt;=&amp;gt;&lt;/code&gt;, 而 Java 中是 &lt;code&gt;-&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 C# 中, lambda 可以使用委托来存储&lt;pre&gt;&lt;code&gt;Action action = () =&amp;gt;
{
    Console.WriteLine(&quot;F**k you, world&quot;);
};
&lt;/code&gt;&lt;/pre&gt;
在 Java 中, 需要使用接口来存储委托&lt;pre&gt;&lt;code&gt;interface Action{
    void execute();    // 方法名随意
}

Action action = () -&amp;gt; {
    System.out.println(&quot;F**k you, world&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;5. 泛型&lt;/h2&gt;
&lt;p&gt;这绝对是 Java 中最让人不爽的地方了.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;你无法创建泛型数组&lt;pre&gt;&lt;code&gt;static &amp;lt;T&amp;gt; T[] createArray(int len){
    return new T[len];                    // 会报错, &apos;类型参数 T 无法直接实例化&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;你无法判断一个对象是否是某个泛型类型&lt;pre&gt;&lt;code&gt;static &amp;lt;T&amp;gt; boolean isType(Object obj){
    return obj instanceof T;              // 会报错
    // 同样, T.class 也是不可用的
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;你无法在使用如下重载:&lt;pre&gt;&lt;code&gt;public class Test&amp;lt;T&amp;gt; {
    void Action(List&amp;lt;T&amp;gt; list){};
    void Action(List&amp;lt;Integer&amp;gt; list){};    // 会报错:两个方法具有同样的擦除
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>[C#] 计算 MD5 SHA1 SHA256 SHA384 SHA512 CRC32</title><link>https://slimenull.com/posts/20210418023855/</link><guid isPermaLink="true">https://slimenull.com/posts/20210418023855/</guid><description>直接贴代码了, 复制即可用, 源码部分来自网络.如果要计算字符串的 MD5 值, 直接 Encoding.UTF8.GetBytes() 然后就可以了using System;using System.IO;using System.Linq;namespace NullLib.HashCalc{    public class HashHelper    {        public static string CalcMd5x32(byte[] bytValue)        </description><pubDate>Sun, 18 Apr 2021 02:38:55 GMT</pubDate><content:encoded>&lt;p&gt;直接贴代码了, 复制即可用, 源码部分来自网络.&lt;/p&gt;
&lt;p&gt;如果要计算字符串的 MD5 值, 直接 Encoding.UTF8.GetBytes() 然后就可以了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System;
using System.IO;
using System.Linq;

namespace NullLib.HashCalc
{
    public class HashHelper
    {
        public static string CalcMd5x32(byte[] bytValue)
        {
            return CalcMd5x32(new MemoryStream(bytValue));
        }
        public static string CalcMd5x16(byte[] bytValue)
        {
            return CalcMd5x16(new MemoryStream(bytValue));
        }
        public static string CalcShax1(byte[] bytValue)
        {
            return CalcShax1(new MemoryStream(bytValue));
        }
        public static string CalcShax256(byte[] bytValue)
        {
            return CalcShax256(new MemoryStream(bytValue));
        }
        public static string CalcShax384(byte[] bytValue)
        {
            return CalcShax384(new MemoryStream(bytValue));
        }
        public static string CalcShax512(byte[] bytValue)
        {
            return CalcShax512(new MemoryStream(bytValue));
        }

        public static string CalcMd5x32(Stream stream)
        {
            var MD5CSP = new System.Security.Cryptography.MD5CryptoServiceProvider();

            byte[] bytHash = MD5CSP.ComputeHash(stream);
            MD5CSP.Clear();

            string sHash = BytesToHex(bytHash);
            return sHash;
        }
        public static string CalcMd5x16(Stream stream)
        {
            return CalcMd5x32(stream).Substring(8, 16);
        }
        public static string CalcShax1(Stream stream)
        {
            var SHA1CSP = new System.Security.Cryptography.SHA1CryptoServiceProvider();

            byte[] bytHash = SHA1CSP.ComputeHash(stream);
            SHA1CSP.Clear();

            string sHash = BytesToHex(bytHash);
            return sHash;
        }
        public static string CalcShax256(Stream stream)
        {
            var SHA256CSP = new System.Security.Cryptography.SHA256CryptoServiceProvider();

            byte[] bytHash = SHA256CSP.ComputeHash(stream);
            SHA256CSP.Clear();

            string sHash = BytesToHex(bytHash);
            return sHash;
        }
        public static string CalcShax384(Stream stream)
        {
            var SHA384CSP = new System.Security.Cryptography.SHA384CryptoServiceProvider();

            byte[] bytHash = SHA384CSP.ComputeHash(stream);
            SHA384CSP.Clear();

            string sHash = BytesToHex(bytHash);
            return sHash;
        }
        public static string CalcShax512(Stream stream)
        {
            var SHA512CSP = new System.Security.Cryptography.SHA512CryptoServiceProvider();

            byte[] bytHash = SHA512CSP.ComputeHash(stream);
            SHA512CSP.Clear();

            string sHash = BytesToHex(bytHash);
            return sHash;
        }

        public static string CalcCrcx32(Stream stream)
        {
            Crc32 calculator = new Crc32();
            byte[] buffer = calculator.ComputeHash(stream);
            calculator.Clear();
            //将字节数组转换成十六进制的字符串形式
            string sHash = BytesToHex(buffer);
            return sHash;
        }

        private static string BytesToHex(byte[] array)
        {
            return string.Join(null, array.Select(v =&amp;gt; Convert.ToString(v, 16).PadLeft(2, &apos;0&apos;)));
        }

        /// &amp;lt;summary&amp;gt;
        /// 提供 CRC32 算法的实现
        /// &amp;lt;/summary&amp;gt;
        private class Crc32 : System.Security.Cryptography.HashAlgorithm
        {
            public const uint DefaultPolynomial = 0xedb88320;
            public const uint DefaultSeed = 0xffffffff;
            private uint hash;
            private uint seed;
            private uint[] table;
            private static uint[] defaultTable;
            public Crc32()
            {
                table = InitializeTable(DefaultPolynomial);
                seed = DefaultSeed;
                Initialize();
            }
            public Crc32(uint polynomial, uint seed)
            {
                table = InitializeTable(polynomial);
                this.seed = seed;
                Initialize();
            }
            public override void Initialize()
            {
                hash = seed;
            }
            protected override void HashCore(byte[] buffer, int start, int length)
            {
                hash = CalculateHash(table, hash, buffer, start, length);
            }
            protected override byte[] HashFinal()
            {
                byte[] hashBuffer = uintToBigEndianBytes(~hash);
                this.HashValue = hashBuffer;
                return hashBuffer;
            }
            public static uint Compute(byte[] buffer)
            {
                return ~CalculateHash(InitializeTable(DefaultPolynomial), DefaultSeed, buffer, 0, buffer.Length);
            }
            public static uint Compute(uint seed, byte[] buffer)
            {
                return ~CalculateHash(InitializeTable(DefaultPolynomial), seed, buffer, 0, buffer.Length);
            }
            public static uint Compute(uint polynomial, uint seed, byte[] buffer)
            {
                return ~CalculateHash(InitializeTable(polynomial), seed, buffer, 0, buffer.Length);
            }
            private static uint[] InitializeTable(uint polynomial)
            {
                if (polynomial == DefaultPolynomial &amp;amp;&amp;amp; defaultTable != null)
                {
                    return defaultTable;
                }
                uint[] createTable = new uint[256];
                for (int i = 0; i &amp;lt; 256; i++)
                {
                    uint entry = (uint)i;
                    for (int j = 0; j &amp;lt; 8; j++)
                    {
                        if ((entry &amp;amp; 1) == 1)
                            entry = (entry &amp;gt;&amp;gt; 1) ^ polynomial;
                        else
                            entry = entry &amp;gt;&amp;gt; 1;
                    }
                    createTable[i] = entry;
                }
                if (polynomial == DefaultPolynomial)
                {
                    defaultTable = createTable;
                }
                return createTable;
            }
            private static uint CalculateHash(uint[] table, uint seed, byte[] buffer, int start, int size)
            {
                uint crc = seed;
                for (int i = start; i &amp;lt; size; i++)
                {
                    unchecked
                    {
                        crc = (crc &amp;gt;&amp;gt; 8) ^ table[buffer[i] ^ crc &amp;amp; 0xff];
                    }
                }
                return crc;
            }
            private byte[] uintToBigEndianBytes(uint x)
            {
                return new byte[] { (byte)((x &amp;gt;&amp;gt; 24) &amp;amp; 0xff), (byte)((x &amp;gt;&amp;gt; 16) &amp;amp; 0xff), (byte)((x &amp;gt;&amp;gt; 8) &amp;amp; 0xff), (byte)(x &amp;amp; 0xff) };
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C#] 关于 Linq 拓展函数对迭代对象进行更改不生效的问题</title><link>https://slimenull.com/posts/20210409100935/</link><guid isPermaLink="true">https://slimenull.com/posts/20210409100935/</guid><description>偶然发现, 在使用 Linq 的 Select 方法时, 如果对被迭代对象进行更改, 那么这个更改是不会生效的</description><pubDate>Fri, 09 Apr 2021 10:09:35 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;偶然发现, 在使用 Linq 的 Select 方法时, 如果对被迭代对象进行更改, 那么这个更改是不会生效的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// 定义我们自己要用的类型
class QWQ
{
    public int qwq;
    public string awa;
    public QWQ(){}
    public QWQ(int qwq){this.qwq=qwq;}
}
// 用来执行的方法
static void LinqExTest()
{
    List&amp;lt;QWQ&amp;gt; qwqs = Enumerable.Range(0, 10).Select(v =&amp;gt; new QWQ(v)).ToList();
    // 在迭代的同时, 对原对象进行更改:
    qwqs.Select(v =&amp;gt; v.awa = v.qwq.ToString());
    // 打印对象
    Console.WriteLine(string.Join(&quot;\n&quot;, qwqs.Select(v =&amp;gt; $&quot;qwq:{v.qwq}, awa:{v.awa}&quot;)));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终执行效果是:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;qwq:0, awa:
qwq:1, awa:
qwq:2, awa:
qwq:3, awa:
qwq:4, awa:
qwq:5, awa:
qwq:6, awa:
qwq:7, awa:
qwq:8, awa:
qwq:9, awa:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显然, 对原对象的更改没有生效! 即便是改为通过索引来访问, 也是不彳亍:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;qwqs.Select((v, i) =&amp;gt; qwqs[i].awa = v.qwq.ToString());
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;qwq:0, awa:
qwq:1, awa:
qwq:2, awa:
qwq:3, awa:
qwq:4, awa:
qwq:5, awa:
qwq:6, awa:
qwq:7, awa:
qwq:8, awa:
qwq:9, awa:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是如果将 Select 语句改为 foreach 语句, 则会成功运行:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;foreach(var v in qwqs)
    v.awa = v.qwq.ToString();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;qwq:0, awa:0
qwq:1, awa:1
qwq:2, awa:2
qwq:3, awa:3
qwq:4, awa:4
qwq:5, awa:5
qwq:6, awa:6
qwq:7, awa:7
qwq:8, awa:8
qwq:9, awa:9
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C#] 二进制, 十进制, 十六进制, 进制转换</title><link>https://slimenull.com/posts/20210406223759/</link><guid isPermaLink="true">https://slimenull.com/posts/20210406223759/</guid><description>在源码中:C# 中允许在代码中使用 0x 开头的十六进制数字, 以及 0b 开头的二进制数字来表示一个整数. 下面的语法是合理的.int a = 123;int b = 0xFF;int c = 0b10010;数字转换将一个数字转换为某进制的字符串, 有两种方式:// 第一种: 使用 Convert 类. 可转换为 二进制 八进制 十进制 十六进制Convert.ToString(10, 2);    // 二进制, 返回 &quot;1010&quot;Convert.ToString(10, 8); </description><pubDate>Tue, 06 Apr 2021 22:37:59 GMT</pubDate><content:encoded>&lt;h3&gt;在源码中:&lt;/h3&gt;
&lt;p&gt;C# 中允许在代码中使用 0x 开头的十六进制数字, 以及 0b 开头的二进制数字来表示一个整数. 下面的语法是合理的.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a = 123;
int b = 0xFF;
int c = 0b10010;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;从数字转换&lt;/h3&gt;
&lt;p&gt;将一个数字转换为某进制的字符串, 有两种方式:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 第一种: 使用 Convert 类. 可转换为 二进制 八进制 十进制 十六进制
Convert.ToString(10, 2);      // 二进制, 返回 &quot;1010&quot;
Convert.ToString(10, 8);      // 八进制, 返回 &quot;12&quot;
Convert.ToString(10, 10);     // 十进制, 返回 &quot;10&quot;
Convert.ToString(10, 16);     // 十六进制, 返回 &quot;a&quot;

// 第二种: 使用对象的 ToString() 方法. 只能转换为 十六进制
10.ToString(&quot;x&quot;);             // 返回 &quot;a&quot;
10.ToString(&quot;X&quot;);             // 返回 &quot;A&quot;
10.ToString(&quot;x2&quot;);            // 返回 &quot;0a&quot;
10.ToString(&quot;X2&quot;);            // 返回 &quot;0A&quot;

// 第三种: 使用 String 的静态方法 Format, 只能转换为 十六进制
string.Format(&quot;{0:x}&quot;, 10)    // 返回 &quot;a&quot;
string.Format(&quot;{0:X}&quot;, 10)    // 返回 &quot;A&quot;
string.Format(&quot;{0:x2}&quot;, 10)   // 返回 &quot;0a&quot;, 即填充至宽2
string.Format(&quot;{0:X2}&quot;, 10)   // 返回 &quot;0A&quot;, 同样填充
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;从字符串转换&lt;/h3&gt;
&lt;p&gt;将字符串转换为对应的数据, 有两种方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 第一种: 使用 Convert类. 可转换 二进制 八进制 十进制 十六进制, 这里以 int 为例
Convert.ToInt32(&quot;1010&quot;, 2);   // 二进制, 返回 10
Convert.ToInt32(&quot;12&quot;, 8);     // 八进制, 返回 10
Convert.ToInt32(&quot;10&quot;, 10);    // 十进制, 返回 10
Convert.ToInt32(&quot;A&quot;, 16);     // 十六进制, 返回 10
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 第二种: 使用对应数据类型的 Parse 方法, 只支持 十六进制, 这里以 int 为例
int.Parse(&quot;A&quot;, NumberStyles.HexNumber);  // 返回 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;从字符数组转换&lt;/h3&gt;
&lt;p&gt;最常用的就是将字符数组转换为十六进制字符串了, Linq 可以帮到我们许多&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;byte[] array; // 假定这个数组有值

// 第一种: 对应上面的 Convert 类静态方法, 也是个人比较推荐的
string.Join(null, array.Select(v =&amp;gt; Convert.ToString(v, 16).PadLeft(2, &apos;0&apos;)));      // 返回小写的

// 第二种: 对应格式化方法
string.Join(null, array.Select(v =&amp;gt; v.ToString(&quot;X2&quot;)));

// 第三种: 对应 String 静态 Format 方法
string.Join(null, array.Select(v =&amp;gt; string.Format(&quot;{0:X2}&quot;, v)));
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[PowerShell] 快速入门, 基本语法, 常用类型, 函数, .NET 互操作</title><link>https://slimenull.com/posts/20210324154212/</link><guid isPermaLink="true">https://slimenull.com/posts/20210324154212/</guid><description>PowerShell 快速入门开始之前, 我们认定你已经有一定的编程基础, 熟悉 .NET 中的类型与对象.此文章对于 .NET 开发者来说更简单哦!在 PowerShell 中, 几乎一切都是对象. 与 CMD 有很大不同. PowerShell 是强类型的, 它基于 .NET, 故, PowerShell 可以近乎完美的调用 .NET 的标准库.0. 官方文档既然要学新东西, 肯定要会查阅官方文档才彳亍呀! 本文章参阅官方文档, 并使用更简单的语言讲述给读者, 在每一部分都会有推荐的官方文</description><pubDate>Wed, 24 Mar 2021 15:42:12 GMT</pubDate><content:encoded>&lt;h1&gt;PowerShell 快速入门&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;开始之前, 我们认定你已经有一定的编程基础, 熟悉 .NET 中的类型与对象.&lt;/p&gt;
&lt;p&gt;此文章对于 .NET 开发者来说更简单哦!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 PowerShell 中, 几乎一切都是对象. 与 CMD 有很大不同. PowerShell 是强类型的, 它基于 .NET, 故, PowerShell 可以近乎完美的调用 .NET 的标准库.&lt;/p&gt;
&lt;h2&gt;0. 准备工作&lt;/h2&gt;
&lt;h3&gt;- 官方文档&lt;/h3&gt;
&lt;p&gt;既然要学新东西, 肯定要会查阅官方文档才彳亍呀! 本文章参阅官方文档, 并使用更简单的语言讲述给读者, 在每一部分都会有推荐的官方文档链接目录, 点击即可跳转.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powershell/scripting/how-to-use-docs?view=powershell-7.1&quot;&gt;MSDocs PowerShell : 如何使用 PowerShell 官方文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powershell/scripting/overview?view=powershell-7.1&quot;&gt;MSDocs PowerShell : PowerShell 是什么&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_arrays?view=powershell-7.1&quot;&gt;MSDocs PowerShell : 关于主题 (涵盖有关 PowerShell 的一系列概念)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;另外, 在文档的中文页面的左侧索引部分, 你无法看到所有的内容, 例如 Reference 分类, 其中包含了非常多的 &apos;隐藏&apos; 内容, 只有你切换到英文页面才可以看到它们.&lt;/p&gt;
&lt;p&gt;至于如何切换页面语言, 也简单, 只需要将地址栏中的 &apos;zh-cn&apos; 改为 &apos;en-us&apos; 即可, 同理, 如果某个页面你看不懂, 想要切换到中文, 可以将 &apos;en-us&apos; 改为 &apos;zh-cn&apos;, 不过需要注意的是, 部分页面是没有提供中文翻译的.&lt;/p&gt;
&lt;p&gt;为了方便, 本文涉及到的所有文档链接均为英文直链, 如需访问中文页面, 请自行修改链接地址.&lt;/p&gt;
&lt;h3&gt;- 启动 PowerShell&lt;/h3&gt;
&lt;p&gt;PowerShell 同 CMD 一样有许多启动方式, 准备好开始了吗? 那么开始吧!&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;按下 &quot;Win&quot; + &quot;R&quot; 打开 &apos;运行&apos; 窗口, 键入 &apos;PowerShell&apos; 并确认.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;按 &quot;Win&quot; + X, 然后按 &apos;I&apos;, 或者按 &apos;A&apos;(以管理员身份运行).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在桌面或资源管理器, 不选中任何条目, 按住 Shift, 右击背景, 在弹出的菜单中选择 &apos;在此处启动 PowerShell&apos;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在资源管理器中, 在地址栏输入 &apos;PowerShell&apos; 并按 Enter 键确认.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来, 出现的蓝底白字的控制台页面就是 PowerShell 的主程序了! 在其中输入指令(代码)可以运行一些东西.&lt;/p&gt;
&lt;h2&gt;1. 基本语法&lt;/h2&gt;
&lt;h3&gt;- 标准输出&lt;/h3&gt;
&lt;p&gt;让我们从一个 &apos;hello world&apos; 的输出开始吧, 与 CMD, Bash 一样, 使用 echo 指令是可以输出内容的.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;Hello World!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hello World!
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 获取帮助:&lt;/h3&gt;
&lt;p&gt;执行以下指令来获取关于 echo 的帮助内容.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Get-Help echo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NAME
    Write-Output

SYNTAX
    Write-Output [-InputObject] &amp;lt;psobject[]&amp;gt; [-NoEnumerate]  [&amp;lt;CommonParameters&amp;gt;]


ALIASES
    write
    echo


REMARKS
    Get-Help cannot find the Help files for this cmdlet on this computer. It is displaying only partial help.
        -- To download and install Help files for the module that includes this cmdlet, use Update-Help.
        -- To view the Help topic for this cmdlet online, type: &quot;Get-Help Write-Output -Online&quot; or
           go to https://go.microsoft.com/fwlink/?LinkID=113427.     
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到结果, 你懂了吧, echo 实际上是 Write-Output 的别名, 同时也可以用 write 来执行.&lt;/p&gt;
&lt;p&gt;另外, PowerShell 的指令是不区分大小写的, 所以 Write-Output 与 wRITE-oUTPUT 是一样执行效果的&lt;/p&gt;
&lt;h3&gt;- 定义变量&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_variables&quot;&gt;PowerShell 官方文档 : 关于变量&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;定义变量的方式与 Bash 差不多, 都是用 $ 开头, 后跟变量名&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$var = 123
echo $var
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;123
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 固定常量&lt;/h3&gt;
&lt;p&gt;PowerShell 中有一些常量, 不可被赋值 (如果对其赋值, 不会产生任何作用, 且不会抛出异常)&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;常量名&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$null&lt;/td&gt;
&lt;td&gt;object&lt;/td&gt;
&lt;td&gt;null&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$true&lt;/td&gt;
&lt;td&gt;bool&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$false&lt;/td&gt;
&lt;td&gt;bool&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果访问一些未定义的变量, 返回的值同样是 null&lt;/p&gt;
&lt;h3&gt;- 类型概念&lt;/h3&gt;
&lt;p&gt;PowerShell 是强类型的. 例如刚刚我们定义的变量, 其实就是一个 int 变量. 示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$a = 123
$b = 321
$c = $a + $b
echo $c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;444
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数字支持运算, 所以理所当然的可以进行运算.&lt;/p&gt;
&lt;h3&gt;- 表达式与指令&lt;/h3&gt;
&lt;p&gt;刚刚我们所用的 echo 指令, 同时可以说是一个表达式. 而刚刚我们进行加法运算时, 所使用的 &apos;$a + $b&apos; 通用&lt;/p&gt;
&lt;p&gt;我没猜错的话, 在刚刚的变量部分, 也同样有人这么执行:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$a = 123
$b = 321
echo $a + $b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后结果就是, 输出了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$a + $b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而解决办法就是, 为这个表达式加上括号, 优先运算这个表达式, 于是就正常了, 或者你可以直接 echo 一个表达式:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo (123+321)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;444
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 运算符&lt;/h3&gt;
&lt;p&gt;刚刚我们从尝试了加法运算, 同时在 PowerShell 中, 也是支持减法, 乘法, 除法的, 除此之外还有小于, 大于, 等于, 大于等于, 小于等于:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo (1 -lt 10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;True
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;运算符&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-eq&lt;/td&gt;
&lt;td&gt;等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-ne&lt;/td&gt;
&lt;td&gt;不等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-lt&lt;/td&gt;
&lt;td&gt;小于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-gt&lt;/td&gt;
&lt;td&gt;大于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-le&lt;/td&gt;
&lt;td&gt;小于等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-ge&lt;/td&gt;
&lt;td&gt;大于等于&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;2. 流程控制:&lt;/h2&gt;
&lt;h3&gt;- 基本判断&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_if&quot;&gt;MSDocs PowerShell : 关于 If&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;判断的话, 毋庸置疑是 if 语句. 当然, 无非是 &apos;if&apos; &apos;else&apos; &apos;else if&apos;&lt;/p&gt;
&lt;p&gt;在 PowerShell 中, if 语句的语法可以是这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (exp) {
    statement block
} elseif {
    statement block
} else {
    statement block
}
# 示例 :
$randint = (New-Object Random).Next()    # 生成一个随机数, 并赋值给 randint 变量
if ($randint -eq 114514) {
    echo &quot;嗯哼哼哼啊啊啊啊啊啊啊啊&quot;
} elseif ($randint -gt 114514) {
    echo &quot;吔屎了你, 梁非凡!&quot;
} else {
    echo &quot;听不见! 就这点声音还想开军舰!?&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没错, PowerShell 中, &apos;else if&apos; 的话是不允许带空格的. 并且需要注意的是, 这个大括号不允许省略.&lt;/p&gt;
&lt;h3&gt;- 循环语句&lt;/h3&gt;
&lt;p&gt;for 循环&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_for?view=powershell-7.1&quot;&gt;MSDocs PowerShell : 关于 For&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;for (exp; exp; exp) {
    statement block
}
# 示例 :
for ($i = 0; $i -lt 10; $i++) {
    echo $i
}
for ($i = 1; $i -lt 10; $i++) {
    for ($j = 1; $j -le $i; $j++) {
        [Console]::Write((&quot;{0}x{1}={2}`t&quot; -f ($j, $i, $i * $j)))   # 不换行输出
    }
    [Console]::WriteLine()    # 换行
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;foreach 循环&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_foreach&quot;&gt;MSDocs PowerShell : 关于 Foreach&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;foreach ($var in enumerable object) {
    statement block
}
# 示例 :
foreach ($num in @(1..5)) {
    echo $num
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;while 循环&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_while&quot;&gt;MSDocs PowerShell : 关于 While&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;while (exp) {
    statement block
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打破循环:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_break&quot;&gt;MSDocs PowerShell : 关于 Break&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;# 在 for 中打破循环 :
for ($i = 0; $i -lt 10; $i++) {
    if ($i -eq 5) {
        break
    }
    echo $i
}

# 在 foreach 语句中打破循环 :
foreach ($i in @(1..10)) {
    if ($i -eq 5) {
        break
    }
    echo $i
}

# 在 while 语句中打破循环 :
$i = 0
while ($true) {
    if ($i -eq 5) {
        break
    }
    echo $i
    $i++
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 常用类型&lt;/h2&gt;
&lt;h3&gt;- 对象数组&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_arrays?view=powershell-7.1&quot;&gt;MSDocs PowerShell : 关于数组&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-arrays?view=powershell-7.1&quot;&gt;MSDocs PowerShell : 关于数组的各项须知内容&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 PowerShell 中, 当然也存在数组, 通过以下方式来创建:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 语法 1:
$var = @(element 1, element 2, element 3, ...., element n)
# 示例:
$array = @(1, 3, 4, 5)

# 语法 2:
$var = start..end
# 示例:
$array = @(1..5)    # 生成的数组等同于: @(1, 2, 3, 4, 5)

# 提示, @() 是可以省略的. 不过如果要创建空数组, 不可省略
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数组的访问, 与其他大多数编程语言一致, 通过中括号就可以:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo @(1..4)[2]   # 输出 3
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;$array = @(1..4)
$array[2] = 114514
echo $array[2]    # 输出 114514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PowerShell 的数组还支持获取多个值, 它返回一个新的数组:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$array = @(1..4)
$array2 = $array[0, 2]
echo $array2[0]   # 输出 1
echo $array2[1]   # 输出 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加元素, 可以用 &apos;+=&apos; 运算符, 但注意, 不推荐这么做, 因为每为数组添加元素, 实际上都是重新创建了新的数组, 如果需要更灵活的数组, 请使用 泛型List&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$array = @(1..4)
$array += 114514
echo $array[4]    # 输出 114514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果获取数组元素值时, 索引超出界限, 不会抛出异常, 会返回 null:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$array = @(1..4)
echo $array[114514]  # 不会有任何输出, 因为值是null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果对一个不是数组的变量使用索引操作符, 则会抛出异常:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$test = $null
$test[0]          # 抛出异常
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个数组相加, 返回数组连接后的结果:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$array1 = @(1..3)
$array2 = @(3..1)
echo ($array1 + $array2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1
2
3
3
2
1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 -contains 操作符来判断数组是否包含某元素:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$array = @(1..4)
echo ($array -contains 2)    # 输出 True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 PowerShell 的数组中, -eq 操作符将对所有元素进行判断, 即, 遍历内容, 逐一进行 -eq 运算, 如若有任何元素运算返回 True, 则表达式返回真.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$array = @(1..4)
echo ($array -eq 3)          # 返回 True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果判断一个变量(可能是数组)为空, 请不要将这个变量放在左侧:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$array -eq $null      # 不要这样使用
$null -eq $array      # 正确的使用方式
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 Count 来获取一个数组的长度, 但注意, 如果一个变量值为null, 同样可以使用 count 来获取长度, 它返回 0:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$array = @(1..4)
echo $array.Count     # 返回 4
echo $null.Count      # 返回 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你也可以通过 Length 来获取一个数组的长度, 它与 Count 用法一样, 并且如果是 $null, 同样返回 0.&lt;/p&gt;
&lt;h3&gt;- 哈希表&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_hash_tables&quot;&gt;MSDocs PowerShell : 关于哈希表&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-hashtable?view=powershell-7.1&quot;&gt;MSDocs PowerShell : 关于哈希表的一些注意事项&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;哈希表, 即 HashTable. 在 PowerShell 中这样创建 HashTable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$table = @{
    key = value
    key = value
}
# 示例 :
$table = @{}            # 创建空表
$table = @{             # 创建有初始值的表, 下面的示例将使用这个表
    &quot;NullSlime&quot; = 100
    &quot;MoYuro&quot; = 96
    &quot;Muchen&quot; = 94
    &quot;KingHans&quot; = 88
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问哈希表, 通过索引运算符, 指定键:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 使用刚刚创建的 table
echo $table[&quot;MoYuro&quot;]    # 输出 96
$table[&quot;MoYuro&quot;] = 97
echo $table[&quot;MoYuro&quot;]    # 输出 97
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;向表中添加键值对, 使用 Add 方法:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$table.Add(key, value)
# 示例 :
$table.Add(&quot;KunLong&quot;, 76)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除表中的某个键值对, 使用 Remove 方法:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$table.Remove(key)
# 示例 :
$table.Remove(&quot;KingHans&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哈希表和数组一样支持选择多个值, 只需要在索引运算符中指定多个键:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$table[key1, key2, ... , key n]
$table[(key1, key2, ... , key n)]
$table[@(key1, key2, ... , key n)]
# 示例 :
$values = $table[&quot;NullSlime&quot;, &quot;MoYuro&quot;]    # 返回一个数组
echo $values[0]                            # 输出 100
echo $values[1]                            # 输出 96
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 Keys 与 Values 来获取这个哈希表的所有键以及值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$keys = $tables.Keys          # 返回哈希表的所有键
$values = $tables.Values      # 返回哈希表的所有值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以通过对哈希表的枚举器来进行循环哈希表的每一个键值对:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;foreach ($pair in $table.GetEnumerator()) {
    echo (&quot;{0}: {1}&quot; -f ($pair.Key, $pair.Value))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当通过foreach对哈希表进行迭代时, 不可以对哈希表进行删减:&lt;/p&gt;
&lt;h2&gt;4. 脚本函数&lt;/h2&gt;
&lt;h3&gt;- 关于函数&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions&quot;&gt;PowerShell 官方文档 : 关于函数&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;PowerShell 中定义函数很简单, 它的本质是脚本块(ScriptBlock):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function name(parameters) {
    statement block
}
# 示例 :
function myFunc($a, $b) {
    echo ($a + $b)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用 PowerShell 中定义的函数, 语法如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name argv1 argv2 argv3 .... argv n
# 示例 :
myFunc 114000 514    # 输出 114514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要获取这个函数的对象引用 可以这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$function:name
# 示例 :
echo $function:myFunc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;param($a, $b)

echo ($a + $b)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过函数的对象引用, 可以使用 Invoke 来调用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$function:name.Invoke(parameters)
# 示例 :
$function:myFunc.Invoke(114000, 514)    # 输出 114514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除一个一定义的函数, 可以使用 del 语句:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;del function:name
# 示例 :
del function:myFunc
echo $function:myFunc     # 由于函数已删除, 所以不会有任何输出
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数可以有返回值, 只需要使用 return 语句:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function myFunc($a, $b) { return $a + $b; }
$result = myFunc 114000 514
echo $result      # 输出 114514
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 面向对象&lt;/h2&gt;
&lt;h3&gt;- 使用对象&lt;/h3&gt;
&lt;p&gt;肯定会有人执行这个指令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo Hello World!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然仅仅少了一对双引号, 但是, 它表示的意思其实跟之前我们写的完全不一样. 如果不加双引号, 那么在这个指令中, Hello 和 World! 会被认为是两个参数! 或者准确来说, 是两个对象!&lt;/p&gt;
&lt;p&gt;这条没有加双引号的指令, 输出结果是:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hello
World!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外, 命令的输出同样也是一个个对象, 执行下面的指令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1+1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- .NET 互操作&lt;/h3&gt;
&lt;p&gt;事实上, PowerShell 中的对象都是 .NET 对象. 并且我们可以进行 .NET 对象中所支持的操作. 执行下面的指令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(1+1).GetType()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int32                                    System.ValueType
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这熟悉的命名空间, 可不就是 .NET 吗?&lt;/p&gt;
&lt;p&gt;试试以下指令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(echo 123).GetType()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int32                                    System.ValueType
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好家伙, 原来 echo 直接就是把这个对象输出出来对吧!&lt;/p&gt;
&lt;p&gt;而事实上, 一个表达式的运算结果(返回值), 是会打印到控制台的, 这点与 Python 一样.&lt;/p&gt;
&lt;p&gt;所以如果你执行一个 &apos;1+1&apos;, 这个表达式的运算结果是 2, 则 2 会被打印到控制台.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1+1  # 输出 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你不希望一个语句的返回值被打印到控制台, 可以在首部添加 [void], 它表示将语句的返回值强制转换为 void 以保证没有输出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[void](1+1)    # 没有输出
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 New-Object 可以创建一个对象, 指定 .NET 对象的全名即可创建:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$random = New-Object System.Random
$randint = $random.Next()    # 调用 System.Random 对象的 Next 方法
echo $randint                # 输出一个随机的数字
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要带参数的创建 .NET 对象, 直接在 New-Object 语句后添加参数数组即可:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$str = New-Object String (&apos;草&apos;, 20)    # 生草
echo $str  # 返回 &apos;草草草草草草草草草草草草草草草草草草草草&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PowerShell 中的强制类型转换是这样使用的:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[类型]对象
# 示例 :
$numArray = [int[]](1..4)   # 将 object[] 转换为 int[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用 .NET 类的静态方法, 使用下面的语法:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[类的全名]::方法名(参数)
# 示例 :
[Console]::WriteLine(&quot;Hello world!&quot;)   # 输出 Hello world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问字段, 也是一样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ansi = [System.Text.Encoding]::Default
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要使用某个命名空间, 使用下面的语句:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using namespace System.Text
$ansi = [Encoding]::Default
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;值得高兴的是, 通过调用 .NET 的组件, 你甚至可以在 PowerShell 中创建 UI 窗体, 下面是一个简单的示例代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

$form = New-Object System.Windows.Forms.Form
$form.Text = &apos;Select a Computer&apos;
$form.Size = New-Object System.Drawing.Size(300,200)
$form.StartPosition = &apos;CenterScreen&apos;

$okButton = New-Object System.Windows.Forms.Button
$okButton.Anchor = [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Right
$okButton.Location = New-Object System.Drawing.Point(75,120)
$okButton.Size = New-Object System.Drawing.Size(75,23)
$okButton.Text = &apos;OK&apos;
$okButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
$form.AcceptButton = $okButton
$form.Controls.Add($okButton)

$cancelButton = New-Object System.Windows.Forms.Button
$cancelButton.Anchor = [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Right
$cancelButton.Location = New-Object System.Drawing.Point(150,120)
$cancelButton.Size = New-Object System.Drawing.Size(75,23)
$cancelButton.Text = &apos;Cancel&apos;
$cancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$form.CancelButton = $cancelButton
$form.Controls.Add($cancelButton)

$label = New-Object System.Windows.Forms.Label
$label.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left
$label.Location = New-Object System.Drawing.Point(10,20)
$label.Size = New-Object System.Drawing.Size(280,20)
$label.Text = &apos;Please select a computer:&apos;
$form.Controls.Add($label)

$listBox = New-Object System.Windows.Forms.ListBox
$listBox.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Right
$listBox.Location = New-Object System.Drawing.Point(10,40)
$listBox.Size = New-Object System.Drawing.Size(260,20)
$listBox.Height = 80

[void] $listBox.Items.Add(&apos;atl-dc-001&apos;)
[void] $listBox.Items.Add(&apos;atl-dc-002&apos;)
[void] $listBox.Items.Add(&apos;atl-dc-003&apos;)
[void] $listBox.Items.Add(&apos;atl-dc-004&apos;)
[void] $listBox.Items.Add(&apos;atl-dc-005&apos;)
[void] $listBox.Items.Add(&apos;atl-dc-006&apos;)
[void] $listBox.Items.Add(&apos;atl-dc-007&apos;)

$form.Controls.Add($listBox)

$form.Topmost = $true

$result = $form.ShowDialog()

if ($result -eq [System.Windows.Forms.DialogResult]::OK)
{
    $x = $listBox.SelectedItem
    [System.Windows.Forms.MessageBox]::Show(&quot;你选择了:&quot; + $x.ToString())
} else {
    [System.Windows.Forms.MessageBox]::Show(&quot;你没有选择任何条目&quot;)
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C#] WPF TextBox 在 PreviewDragEnter 中指定 Effect 且指定 Handled 为 true, 仍然无法正常实现拖拽操作.</title><link>https://slimenull.com/posts/20210319194401/</link><guid isPermaLink="true">https://slimenull.com/posts/20210319194401/</guid><description>在开始之前, 请先阅读这篇文章: [C#] WPF Preview 事件与普通事件的区别我们知道, 某些控件会对事件进行处理, 导致部分事件我们无法正常使用, 对于 TextBox, 显而易见的是关于拖拽的事件完全不能正常使用. 因而我们需要使用 Preview 事件.对于一套拖拽操作, 有以下过程:用户拖拽数据进入控件 (DragEnter)用户拖拽数据在控件上移动 (DragOver)用户拖拽数据在控件上松开鼠标 (Drop)对于一个控件, 必须指定这个控件的 AllowDrop 属性为</description><pubDate>Fri, 19 Mar 2021 19:44:01 GMT</pubDate><content:encoded>&lt;p&gt;在开始之前, 请先阅读这篇文章: &lt;a href=&quot;/posts/20210319193001/&quot;&gt;[C#] WPF Preview 事件与普通事件的区别&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们知道, 某些控件会对事件进行处理, 导致部分事件我们无法正常使用, 对于 TextBox, 显而易见的是关于拖拽的事件完全不能正常使用. 因而我们需要使用 Preview 事件.&lt;/p&gt;
&lt;p&gt;对于一套拖拽操作, 有以下过程:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户拖拽数据进入控件 (DragEnter)&lt;/li&gt;
&lt;li&gt;用户拖拽数据在控件上移动 (DragOver)&lt;/li&gt;
&lt;li&gt;用户拖拽数据在控件上松开鼠标 (Drop)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于一个控件, 必须指定这个控件的 AllowDrop 属性为 true, 这个控件才可以接受拖拽操作&lt;/p&gt;
&lt;p&gt;对于上述 3 个事件:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;DragEnter 中, 我们需要写的是判断拖拽的数据的类型, 并设定拖拽效果(DragDropEffects), 指定是否已经处理当前事件(Handled)&lt;/li&gt;
&lt;li&gt;DragOver 中, 与 DragEnter 中一致. 所以这两个事件, 可以通过同一个方法来进行订阅&lt;/li&gt;
&lt;li&gt;Drop 中, 应该写对数据的处理, 例如将数据添加到界面中&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意! 在 DragEnter 和 DragOver 中, 必须指定 DragDropEffects 才表示允许拖拽. 如果不对 Effects 进行赋值, 那么 Drop 方法将不会被触发.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;相信看完上面的内容, 你已经可以解决你的问题了, 如果无法解决问题, 请在下方评论, 我会继续完善这篇文章&lt;/p&gt;
</content:encoded></item><item><title>[C#] WPF Preview 事件与普通事件的区别.</title><link>https://slimenull.com/posts/20210319193001/</link><guid isPermaLink="true">https://slimenull.com/posts/20210319193001/</guid><description>很多文章都提到了冒泡事件和隧穿事件, 我是没有去测试过这两个的, 但是有一个非常非常重要的点很多人都忽略了.已预处理事件的控件在 WPF 中, 部分控件已经对某些事件进行了处理, 例如一个 Button, 它提供了 Click 事件, 而 Click 的本质是 MouseDown 和 MouseUp, 因而, Button 的 MouseDown 和 MouseUp 事件是没办法正常使用的…如果需要使用它们, 你得使用 PreviewMouseDown 和 PreviewMouseUp.同样, 有很</description><pubDate>Fri, 19 Mar 2021 19:30:01 GMT</pubDate><content:encoded>&lt;p&gt;很多文章都提到了冒泡事件和隧穿事件, 我是没有去测试过这两个的, 但是有一个非常非常重要的点很多人都忽略了.&lt;/p&gt;
&lt;h3&gt;已预处理事件的控件&lt;/h3&gt;
&lt;p&gt;在 WPF 中, 部分控件已经对某些事件进行了处理, 例如一个 Button, 它提供了 Click 事件, 而 Click 的本质是 MouseDown 和 MouseUp, 因而, Button 的 MouseDown 和 MouseUp 事件是没办法正常使用的...&lt;/p&gt;
&lt;p&gt;如果需要使用它们, 你得使用 PreviewMouseDown 和 PreviewMouseUp.&lt;/p&gt;
&lt;p&gt;同样, 有很多控件都是这样的, 例如 TextBox, 一个支持输入文本的控件, 同样还支持拖拽事件, 你可以向其中拖拽文本, 但是你不可以向其中拖拽图片, 这也是因为 TextBox 对拖拽事件进行了处理.&lt;/p&gt;
&lt;p&gt;也正因为如此, 即便你 TextBox 的 DragEnter 事件中指定了允许拖拽, 你也是不可以进行拖拽的! 因为 WPF 已经对这个事件进行处理了, 我们的逻辑不会起作用.&lt;/p&gt;
</content:encoded></item><item><title>[Python] 巧用! 局域网传输文件! 还可以这么玩!!!</title><link>https://slimenull.com/posts/20210319184242/</link><guid isPermaLink="true">https://slimenull.com/posts/20210319184242/</guid><description>之前… 我知道了 Python 有着内置的网络库, 甚至可以直接用来原地开一个 http 服务器!最近… 频繁的需要在局域网内传输大文件… 用 U 盘当然可以, 但完全没必要, 太浪费了也试着用 Python 的套接字来手动将文件数据发送到另一台机器, 然后另一台机器也用 Python 套接字来接收, 并直接写入到文件… 好吧我知道很麻烦没错然后… 我突然想起… Python 开那个 HTTP 服务器可不就能直接用来分享文件啊卧槽?于是…python -m http.server成功了… 而且</description><pubDate>Fri, 19 Mar 2021 18:42:42 GMT</pubDate><content:encoded>&lt;p&gt;之前... 我知道了 Python 有着内置的网络库, 甚至可以直接用来原地开一个 http 服务器!&lt;/p&gt;
&lt;p&gt;最近... 频繁的需要在局域网内传输大文件... 用 U 盘当然可以, 但完全没必要,,, 太浪费了
也试着用 Python 的套接字来手动将文件数据发送到另一台机器, 然后另一台机器也用 Python 套接字来接收, 并直接写入到文件... 好吧我知道很麻烦没错&lt;/p&gt;
&lt;p&gt;然后... 我突然想起... Python 开那个 HTTP 服务器可不就能直接用来分享文件啊卧槽?&lt;/p&gt;
&lt;p&gt;于是...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python -m http.server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功了... 而且速度不赖~&lt;/p&gt;
</content:encoded></item><item><title>[.NET] 拓展库, 开源库, nuget库收集. (长期更新)</title><link>https://slimenull.com/posts/20210319121204/</link><guid isPermaLink="true">https://slimenull.com/posts/20210319121204/</guid><description>如果感兴趣, 可以试试下面的库名称标签描述Newtonsoft.JsonJSON分析, 文本处理非常流行的JSON操作库, 教程也能够轻松在网上找到, 速度也很不错NAudio音频处理, 录音, 播放音频流行而又强大的音频操作库Fody.Costura程序打包仅需安装这个库, 即可直接使所有需要的程序集内嵌入你的程序, 实现单文件程序System.Drawing.Common图像处理你可能会发现 .NET Core 不内置 System.Drawin</description><pubDate>Fri, 19 Mar 2021 12:12:04 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;如果感兴趣, 可以试试下面的库&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;标签&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Newtonsoft.Json&lt;/td&gt;
&lt;td&gt;JSON分析, 文本处理&lt;/td&gt;
&lt;td&gt;非常流行的JSON操作库, 教程也能够轻松在网上找到, 速度也很不错, 是 ASP.NET 中 Json 序列化反序列化的默认使用方式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NAudio&lt;/td&gt;
&lt;td&gt;音频处理, 录音, 播放音频&lt;/td&gt;
&lt;td&gt;流行而又强大的音频操作库, 支持音频播放, 录制, 转码, 以及音频可视化, &lt;a href=&quot;/posts/20210506194904/&quot;&gt;NAudio 各种使用方式&lt;/a&gt;, &lt;a href=&quot;/posts/20210509234918/&quot;&gt;NAudio 实现音频可视化&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Costura.Fody&lt;/td&gt;
&lt;td&gt;程序打包&lt;/td&gt;
&lt;td&gt;仅需安装这个库, 即可直接使所有需要的程序集内嵌入你的程序, 实现单文件程序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System.CommandLine&lt;/td&gt;
&lt;td&gt;命令行参数分析&lt;/td&gt;
&lt;td&gt;微软官方推出的命令行分析, 截止到目前, 已经有了预发布版本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PropertyChanged.Fody&lt;/td&gt;
&lt;td&gt;自动处理&lt;/td&gt;
&lt;td&gt;安装这个库后, 所有继承 INotifyPropertyChanged 接口的类都将自动添加通知属性变更的逻辑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WinApi&lt;/td&gt;
&lt;td&gt;Win32&lt;/td&gt;
&lt;td&gt;C# 的 WinApi 封装, 涵盖的方面非常广, 项目地址是: &lt;a href=&quot;https://github.com/prasannavl/WinApi&quot;&gt;github.com/prasannavl/WinApi&lt;/a&gt;, 下面有很多使用示例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ConsoleGUI&lt;/td&gt;
&lt;td&gt;控制台应用&lt;/td&gt;
&lt;td&gt;适用于控制台程序的界面库, 能够实现控制台上的各种 UI 控件, 例如文本框, TabControl, 可滚动的文本框, 输入框, 消息框, 还有各种布局方式供使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Html Agility Pack&lt;/td&gt;
&lt;td&gt;HTML解析&lt;/td&gt;
&lt;td&gt;非常方便的HTML解析库, 支持通过XPath来查找节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System.Drawing.Common&lt;/td&gt;
&lt;td&gt;图像处理&lt;/td&gt;
&lt;td&gt;你可能会发现 .NET Core 不内置 System.Drawing 程序集 ,那么安装这个吧, 这个是由微软官方发布的功能等同于 System.Drawing 的库&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FiddlerCore&lt;/td&gt;
&lt;td&gt;著名抓包神器 Fiddler 的核心&lt;/td&gt;
&lt;td&gt;这个在 nuget.org 中搜索不到, 但他存在, 只是被隐藏了, 你可以访问 &lt;a href=&quot;https://www.nuget.org/packages/FiddlerCore&quot;&gt;nuget.org/packages/FiddlerCore&lt;/a&gt; 查看他的信息, 通过 nuget 控制台来手动安装它&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NullLib.TickAnimation&lt;/td&gt;
&lt;td&gt;动画&lt;/td&gt;
&lt;td&gt;这个严格来说, 只是对属性按照一定时间函数设置值, 但这正是关键帧动画, 例如通过这个, 你可以实现自动从 0 设置 1, 中间有无数过渡值, 而且可以指定时间函数, 跟前端的贝塞尔曲线一样, 所以很推荐用这个来实现 WinForm 程序的动画实现. 项目地址: &lt;a href=&quot;https://github.com/SlimeNull/NullLib.TickAnimation&quot;&gt;github.com/SlimeNull/NullLib.TickAnimation&lt;/a&gt;, 其中包含了完整的中文使用文档.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NullLib.ConsoleEx&lt;/td&gt;
&lt;td&gt;控制台操作拓展&lt;/td&gt;
&lt;td&gt;支持你在读取用户输入时就获取用户的输入内容, 并且支持字符的过滤, 移动输入内容到任意位置等功能; 或者测量一段文本在控制台中的显示长度(中文, 日文等字符为2, 字母数字为1之类的)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NullLib.CommandLine&lt;/td&gt;
&lt;td&gt;命令行工具&lt;/td&gt;
&lt;td&gt;如果你在开发一个服务端程序, 并且想要使用户能在服务端运行时可以通过输入命令行来实现特定操作, 这个是非常方便的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NullLib.PhantomTank&lt;/td&gt;
&lt;td&gt;幻影坦克&lt;/td&gt;
&lt;td&gt;好耶! 是幻影坦克! 来试试用这个库来快捷生成幻影坦克图片吧!&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EleCho.Json&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;简单方便与快捷的第三方 JSON 库, 支持弱类型操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft.EntityFrameworkCore&lt;/td&gt;
&lt;td&gt;ORM&lt;/td&gt;
&lt;td&gt;对象关系映射, 懒得写 SQL 的话, 就直接用这个吧&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dapper&lt;/td&gt;
&lt;td&gt;ORM&lt;/td&gt;
&lt;td&gt;如果觉得 EFCore 性能不够, 可以试试这个, 但是需要手写 SQL, 而且字段和列名必须保持一致&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft.Playwright&lt;/td&gt;
&lt;td&gt;自动化测试&lt;/td&gt;
&lt;td&gt;用来做爬虫的话, 也是非常方便的, 感觉使用起来要比 Puppeteer 要好用些&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;p&gt;如果你知道有意思的库, 在评论下留下它的名字, 我们一起完善这个收集.&lt;/p&gt;
</content:encoded></item><item><title>[笔记] 记录一次该死的 “玄学“ Bug, 赶紧看看避开这个坑!!! .NET Core, Delegate.BeginInvoke, PlatformNotSupportedException</title><link>https://slimenull.com/posts/20210309214803/</link><guid isPermaLink="true">https://slimenull.com/posts/20210309214803/</guid><description>概述:没事闲着别总是玩异步, 否则可能就会像我这样出现线程问题这件事:首先是… 报了一堆 “平台不支持的” 错误.堆栈显示也看不出来是哪里的问题, 但我看到了 Threading 这玩意儿…:最后通过 “死亡断点” 发现是这里出的问题, 这是一个异步操作的回调函数.进一步调试, 发现是这里, 这里又会 Invoke 一个事件:好家伙, 又是一个事件… 然后我看了看订阅了这个事件的地方…乍一看, 没啥毛病! 但问题确实出在这里! (我实在是太菜了)最后… 我思</description><pubDate>Tue, 09 Mar 2021 21:48:03 GMT</pubDate><content:encoded>&lt;h2&gt;概述:&lt;/h2&gt;
&lt;p&gt;.NET Core 不支持委托的 BeginInvoke 方法, 使用别的方法, 例如 Task 来替代它!&lt;/p&gt;
&lt;h2&gt;这件事:&lt;/h2&gt;
&lt;p&gt;首先是... 报了一堆 &quot;平台不支持的&quot; 错误.&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=&quot;center&quot;&amp;gt;
&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20210309213418546.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;堆栈显示也看不出来是哪里的问题, 但我看到了 Threading 这玩意儿...:&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=&quot;center&quot;&amp;gt;
&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20210309213525619.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;最后通过 &quot;死亡断点&quot; 发现是这里出的问题, 这是一个异步操作的回调函数.&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=&quot;center&quot;&amp;gt;
&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20210309213723524.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;进一步调试, 发现是这里, 这里又会 Invoke 一个事件:&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=&quot;center&quot;&amp;gt;
&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20210309213924137.png&quot;/&amp;gt;
&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20210309214018874.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;好家伙, 又是一个事件... 然后我看了看订阅了这个事件的地方...&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=&quot;center&quot;&amp;gt;
&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20210309214243836.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;乍一看,,,, 没啥毛病! 但问题确实出在这里! (我实在是太菜了)
最后... 我思考了下, 我在 Invoke 调用的委托里面, 整了异步的操作...... 于是我试着将 BeginInvoke 换成 Invoke, 然后.... 问题解决了!!!&lt;/p&gt;
&lt;p&gt;后来又双叒叕遇到了这个问题,,, 然后仔细搜索了下资料. 懂了. 原来 .NET Core 不支持委托的 BeginInvoke 方法. 而事件的本质就是多播委托!&lt;/p&gt;
</content:encoded></item><item><title>[WPF] Matrix Transform, 矩阵变换. 最最最基础的原理解释.</title><link>https://slimenull.com/posts/20210308115948/</link><guid isPermaLink="true">https://slimenull.com/posts/20210308115948/</guid><description>关于向量:1. 向量的基在计算机科学中, 向量, Vector, 通常这么表示:[xy]\left[\begin{array}{cc}x\\y\end{array}\right][xy​]向量有两个 “基”, i‾\overline{i}i, 即 1,0→\overrightarrow{1, 0}1,0​, j‾\overline{j}j​, 即 0,1→\overrightarrow{0, 1}0,1​向量可以看作一组数乘以这些基的结果, 即: v‾=a×i‾+b×j‾\ov</description><pubDate>Mon, 08 Mar 2021 11:59:48 GMT</pubDate><content:encoded>&lt;h2&gt;关于向量:&lt;/h2&gt;
&lt;h3&gt;1. 向量的基&lt;/h3&gt;
&lt;p&gt;在计算机科学中, 向量, Vector, 通常这么表示:
$$
\left[
\begin{array}{cc}
x\
y
\end{array}
\right]
$$
向量有两个 &quot;基&quot;, $\overline{i}$, 即 $\overrightarrow{1, 0}$, $\overline{j}$, 即 $\overrightarrow{0, 1}$&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=&quot;center&quot;&amp;gt;
&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20210308085632860.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;向量可以看作这些基乘以一组数的结果, 即: $\overline{v} = \overline{i} \times a + \overline{j} \times b$, 例如 $[1, 3]$, 就是:
$$
\overline{i} \times 1 + \overline{j} \times 3
= [1, 0] \times 1 + [0, 1] \times 3
= [1, 0] + [0, 3]
= [1, 3]
$$&lt;/p&gt;
&lt;h3&gt;2. 改变向量的基&lt;/h3&gt;
&lt;p&gt;当改变向量的基时, 由于向量是一组数与基的乘法, 所以向量也会随之变化.&lt;/p&gt;
&lt;p&gt;例如我们设定两个基为它们顺时针旋转90°后的结果, 即: $\overline i = [0, -1], \overline j = [1, 0]$, 那么 [1, 3] 就变成了:
$$
\overline i \times 1 + \overline j \times 3
= [0, -1] \times 1 + [1, 0] \times 3
= [0, -1] + [3, 0]
=[3, -1]
$$
你会发现, 这个向量也随之改变了, 而且恰好是顺时针旋转90°&lt;/p&gt;
&lt;h3&gt;3. 矩阵的乘法:&lt;/h3&gt;
&lt;p&gt;在计算机科学中, 向量如此表示:
$$
\left[
\begin{array}{c}
x\y
\end{array}
\right]
$$
如果是两个向量, 则是这样, 竖着的, 是一个向量:
$$
\left[
\begin{array}{c}
x_1, &amp;amp; x_2\
y_1, &amp;amp; y_2
\end{array}
\right]
$$
而, 我们刚刚进行的乘法, 其实也是矩阵乘法, 即:
$$
\left[
\begin{array}{c}
0, &amp;amp; -1\
1, &amp;amp; 0
\end{array}
\right]
\times
\left[
\begin{array}{c}
1\ 3
\end{array}
\right]=
\left[
\begin{array}{c}
3 \ -1
\end{array}
\right]
$$&lt;/p&gt;
&lt;h2&gt;矩阵变换&lt;/h2&gt;
&lt;p&gt;在 WPF 中, 一个矩阵(Matrix)有以下属性: M11, M12, M21, M22, OffsetX, OffsetY.&lt;/p&gt;
&lt;p&gt;其中, M11, M12, M21, M22 表示缩放旋转矩阵:
$$
\left[
\begin{array}{c}
M11, &amp;amp; M12 \
M21, &amp;amp; M22
\end{array}
\right]
$$
它们的默认值是:
$$
\left[
\begin{array}{c}
1, &amp;amp; 0 \
0, &amp;amp; 1
\end{array}
\right]
$$
而进行矩阵变换, 也就是将源图形的每一个点, 与这个矩阵相乘, 最终得到另一些点, 构成一个新的图形.&lt;/p&gt;
&lt;p&gt;而与默认的这个矩阵相乘, 形状不会有任何变化.&lt;/p&gt;
&lt;p&gt;这个矩阵的 M11 和 M21 值, 可以理解为 $\overline i$, M12 和 M22 可以理解为 $\overline j$, 之前我们提到, 如果变化这两个基的值, 那么最终向量也会发生变化, 而当我们将刚刚旋转 90° 后的 $\overline i$ 和 $\overline j$ 拿出来直接代入, 也可以发现, 图形直接旋转了 90°.&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=&quot;center&quot;&amp;gt;
&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20210308105727368.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;矩阵的基本原理就是矩阵的乘法, 但即便你不理解矩阵的乘法, 去改变 Matrix 中的两个基值, 形状也将跟随基值发生改变.&lt;/p&gt;
&lt;p&gt;而 OffsetX 和 OffsetY, 这两个指定了这两个图形的平移, OffsetX 水平偏移量, OffsetY 垂直偏移量.&lt;/p&gt;
&lt;h2&gt;0. 参考内容:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1ys411472E&quot;&gt;哔哩哔哩 3Blue1Brown 线性代数&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ruanyifeng.com/blog/2015/09/matrix-multiplication.html&quot;&gt;理解矩阵乘法 - 阮一峰的网络日志&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/ljy-endl/p/11411665.html&quot;&gt;矩阵乘法 - endl&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>[干货] 一篇文章完全搞懂面向对象基本概念</title><link>https://slimenull.com/posts/20210307161550/</link><guid isPermaLink="true">https://slimenull.com/posts/20210307161550/</guid><description>今天我一个朋友, 突然问我, 在程序入口所用的类中, 为什么字段需要加static…好家伙, 一听就是没懂啥是面向对象! 我也看了网上的一大堆东西, 啊说啥继承, 多态, 反正我是菜鸟的时候是没听懂这些东西, 后来还是我自己摸索出来的 (嘤嘤嘤QAQ)好的, 我们使用 C# 来做演示, 大概了解一下类的最基本概念. 来整一个, 数组拓展.演示代码片:public class ArrayHelper{    public int[] Source;          // 字段    public st.</description><pubDate>Sun, 07 Mar 2021 16:15:50 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;今天我一个朋友, 突然问我, 在程序入口所用的类中, 为什么字段需要加static...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;好家伙, 一听就是没懂啥是面向对象! 我也看了网上的一大堆东西, 啊说啥继承, 多态, 反正我是菜鸟的时候是没听懂这些东西, 后来还是我自己摸索出来的 (嘤嘤嘤QAQ)&lt;/p&gt;
&lt;p&gt;好的, 我们使用 C# 来做演示, 大概了解一下类的最基本概念. 来整一个, 数组拓展.&lt;/p&gt;
&lt;h2&gt;演示代码片:&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public class ArrayHelper
{
    public int[] Source;          // 字段
    public static int InsCount = 0;
    public Arrayhelper()           // 构造函数
    {
        this.Source = new int[]{};
        InsCount++;
    }
    public ArrayHelper(int[] arr)  // 有参数的构造函数
    {
        this.Source = arr;
        InsCount++;
    }
    ~ ArrayHelper()                // 析构函数
    {
        InsCount--;
    }
    public void ResetInsCount()
    {
        InsCount = 0;
    }
    public int GetSum()            // 方法
    {
        int sum = 0;
        foreach (var i in Source)
            sum += i;
        return sum;
    }
    public int GetAverage()        // 方法
    {
        return GetSum() / Source.Length;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;1. 基本概念&lt;/h1&gt;
&lt;h2&gt;1. 啥是对象:&lt;/h2&gt;
&lt;p&gt;对象, 它的英文是 object, 咱也不懂为啥它被翻译成这种有歧义的词, 没错, 它跟 &apos;情人&apos; 没有任何关系. 意思接近为: &quot;物体&quot;, &quot;事物&quot;, 下面的句子中, &apos;对象&apos; 一词是最接近编程中的 &apos;对象&apos; 的.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;你要帮助的对象是谁?&lt;/li&gt;
&lt;li&gt;我们本次整改所针对的对象, 是厂里干部遇事却不作为的事件.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;生活中一切东西都可成为对象, 例如一台电脑, 可以说是一个对象, 它有一些成员(字段, 属性, 方法), 例如所装载的硬件. 而对象都有自身的行为(方法), 例如, 汽车, 有点火这个行为.&lt;/p&gt;
&lt;h2&gt;2. 编程中的类:&lt;/h2&gt;
&lt;p&gt;编程中的类, 我们可以理解为模板, 或者生产机器, 通过它我们可以创建一个对象(类的实例化). 一个类产生的对象, 就是这个类的实例.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ArrayHelper helper = new ArrayHelper();   // 实例化 ArrayHelper, helper 就是我们示例化产生的对象
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 构造函数:&lt;/h2&gt;
&lt;p&gt;构造函数, 就是这个类在构造对象的时候会调用的函数, 例如刚刚 new ArrayHelper() 的时候, 必定会调用类中的这一片代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Arrayhelper()           // 构造函数
{
    this.Source = new int[]{};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以声明多个构造函数, 以使用不同的方式来创建对象.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ArrayHelper(int[] arr)  // 有参数的构造函数
{
    this.Source = arr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让我们来使用下这个带参数的构造函数:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int[] myArray = new int[] {1, 2, 3};
ArrayHelper helper = new ArrayHelper(myArray);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 对象的字段&lt;/h2&gt;
&lt;p&gt;在我们的实例代码中, 类中有写一个 int[] 类型的 Source 字段. 我们也说过, 类就好比一个模板. 当一个对象创建后, 它就拥有这个字段&lt;/p&gt;
&lt;p&gt;当我们实例化之后, 我们就可以通过实例, 来访问新生成的对象的成员:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ArrayHelper helper = new ArrayHelper();
Console.WriteLine(helper.Source);         // 将 helper 的 Source 成员打印出来
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个成员, 是对象所拥有的, 而不是类本身所拥有的, 所以, 下面的代码是错误的:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Console.WriteLine(ArrayHelper.Source);    // 会报错, 因为成员是对象的.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每一个对象都有自己独自的成员, 所以, 不同实例的成员, 值是一定相不同的.&lt;/p&gt;
&lt;h2&gt;5. 对象的方法&lt;/h2&gt;
&lt;p&gt;在我们的示例代码中, 类中有写一个返回值为 int 的 GetSum 方法, 同字段一样, 每个对象都有自己独自的方法, 所以调用后, 返回值是不一定相同的.&lt;/p&gt;
&lt;p&gt;下面是演示:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int[] array1 = new[] {1, 2, 3};
int[] array2 = new[] {1, 2, 3, 4, 5};
ArrayHelper helper1 = new ArrayHelper(array1);
ArrayHelper helper2 = new ArrayHelper(array2);
Console.WriteLine(helper1.GetSum());       // 结果是6
Console.WriteLine(helper2.GetSum());       // 结果是15
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样, 属于对象的方法, 是不能通过类名来调用的, 以下代码是错误的:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Console.WriteLine(ArrayHelper.GetSum());    // 报错, 因为成员是对象的
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 类的字段:&lt;/h2&gt;
&lt;p&gt;类也是可以有成员的, 它们独属于某个对象, 它们属于这个类. 这种成员, 被称为静态成员. 例如我们示例代码中的 InsCount 字段.&lt;/p&gt;
&lt;p&gt;静态成员是可以被所有实例所访问的, 例如构造函数里面我们指定了为 InsCount 的值增加1, 这就意味着, 我们每实例化一个对象, 这个类的 InsCount 的值都将增加1;&lt;/p&gt;
&lt;p&gt;访问类的静态成员, 需要通过类名来访问, 毕竟这个静态成员是属于这个类的. 不可以用实例名来访问, 因为不独属于任何一个实例.&lt;/p&gt;
&lt;p&gt;正确示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Console.WriteLine(ArrayHelper.InsCount);     // 打印类的 InsCount 字段
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;错误示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ArrayHelper helper = new ArrayHelper();
Console.WriteLine(helper.InsCount);        // 报错, 因为成员属于类而不是对象
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7. 类的方法:&lt;/h2&gt;
&lt;p&gt;静态方法, 同静态字段一样, 属于整个类, 所有对象可访问, 只能通过类名而无法通过对象名来访问.&lt;/p&gt;
&lt;p&gt;例如我们的示例代码中的 ResetInsCount 函数.&lt;/p&gt;
&lt;p&gt;正确示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ArrayHelper.ResetInsCount();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;错误示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ArrayHelper helper = new ArrayHelper();
helper.ResetInsCount();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8. 析构函数:&lt;/h2&gt;
&lt;p&gt;析构函数与构造函数相对应, 析构函数将在对象被释放的时候执行, 例如我们代码片中的析构函数:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~ ArrayHelper()
{
    InsCount--;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即, 每当一个对象被释放, InsCount 的值都会减少1. 配合构造函数来看, 造成的结果就是, InsCount 字段将始终与当前的实例数量保持一致.&lt;/p&gt;
&lt;h2&gt;9. this 关键字:&lt;/h2&gt;
&lt;p&gt;有时候啊, 你可能写函数的参数名, 写了个跟成员一模一样的名字, 结果你想给实例成员赋值的时候, 发现实际上确是给方法的参数赋值了.&lt;/p&gt;
&lt;p&gt;在一个类的非静态方法中, 想要访问实例的成员, 你可以用 this 来修饰以避免歧义.&lt;/p&gt;
&lt;p&gt;下面是使用了 this 关键字的示例代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class TestObj
{
    int value;
    public void SetValue(int value)
    {
        this.value = value;
    }
    public int GetValue()
    {
        return this.value;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;10. 成员访问限制:&lt;/h2&gt;
&lt;p&gt;成员有访问限制, 常用的有 公共的(public), 私有的(private), 受保护的(protected), 如果不写修饰符, 那么默认访问限制就是private, 这个我得用一个比较长的代码片来演示了.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;修饰符&lt;/th&gt;
&lt;th&gt;特征&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public&lt;/td&gt;
&lt;td&gt;公有的, 无论谁都可以访问&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;internal&lt;/td&gt;
&lt;td&gt;内部的, 位于同一程序集的可以进行访问&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;protected&lt;/td&gt;
&lt;td&gt;受保护的, 除了这个类的成员, 子类也可以进行访问&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;private&lt;/td&gt;
&lt;td&gt;私有的, 仅有这个类的成员可以访问&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;using System;

namespace Null.Tutorial
{
    class TestObj{
    {
        static int insCount = 0;      // 没有写访问限制修饰符, 但是默认是private
        public int[] Source;
        public TestObj(int[] source)
        {
            this.Source = source;
            insCount++;
        }
        ~TestObj()
        {
            insCount--;
        }
        public int GetSum()
        {
            int sum = 0;
            foreach(var i in Source)
                sum += i;
            return sum;
        }
        public static int GetInstanceCount()
        {
            return insCount;
        }
    }
    class Program
    {
        public static void Main(string[] args)
        {
            int[] myArr = new int[] {1, 2, 3};
            TestObj objIns = new TestObj(myArr);
            Console.WriteLine(objIns.insCount);    // 报错, 因为在类之外访问私有成员
            Console.WriteLine(objIns.GetSum());    // 正常访问, 因为是public
            Console.WriteLine(TestObj.GetInstanceCount());   // 正常访问, 因为是public
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;11. 属性语法糖&lt;/h2&gt;
&lt;p&gt;C# 还支持属性这种东西. 其实是语法糖. 属性类似于字段, 举个例子, 你就懂了.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class TestObj
{
    int field3, field4, field5, field6;
    public int Field1 { get; set; }
    public int Field2 { get; private set; }
    public int Field3 =&amp;gt; field3
    public int Field4 { get =&amp;gt; field4; set =&amp;gt; field4 = value; }
    public int Field5
    {
        get
        {
            return field5;
        }
        set
        {
            field5 = value;
        }
    }
    public int Field6 { get =&amp;gt; field6; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码中, Field1 ~ Field5 都是属性.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Field1 是开放(public)获取值与设置值的属性, 用法跟字段一致.&lt;/li&gt;
&lt;li&gt;Field2 是开放(public)获取值, 但设置值是私有的(private)属性, 即, 在类外面可以直接获取值, 但无法设置.&lt;/li&gt;
&lt;li&gt;Field3 对应私有字段 field3, 它的获取权限是 public, 设置权限是 private&lt;/li&gt;
&lt;li&gt;Field4 对应私有字段 field4, 获取权限是 public, 设置权限是 private&lt;/li&gt;
&lt;li&gt;Field5 是属性的完整写法, 其中包含 get 访问器与 set 访问器, 在没有指定个别访问修饰符时, 它们的权限与整个属性的权限一致.&lt;/li&gt;
&lt;li&gt;Field6 是与私有字段 field6 相对应的, 没有指定 set 访问器的属性, 这意味着它只支持获取值, 不允许设置值, 但是类之内的成员可以直接通过对 field6 进行设置来实现更改属性值.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;12. 对象引用&lt;/h2&gt;
&lt;p&gt;C# 和 Java 中, 有值类型和引用类型, 事实上, 一个 &quot;存储&quot; 对象的字段, 它包含的是这个对象的引用. 这个的话, C++ 程序员肯定熟悉的一批, 可不就是指针嘛.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Object obj = new Object();    // 创建一个对象, 并使用 obj 字段来存储对象引用
Console.WriteLine(obj);       // 通过这个对象引用, 可以直接对对象进行操作.
obj = null;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后一句, 对 obj 字段赋值 null, 肯定会有人以为, 执行完之后, 我们刚刚创建的对象就嗝屁了, 但其实是, 仅仅是这个字段的值为null了.&lt;/p&gt;
&lt;p&gt;引用类型的字段就像一个指针, 它有自己的值, 这个值表示对象的内存地址(也有可能是句柄啥的), 总之有这个引用, 就能够找到对象所在的位置. 而给这个字段赋值为null, 也就是改变了这个指针的方向, 让它指向 0x0000, 但事实上对象还存在着.&lt;/p&gt;
&lt;p&gt;但是呢, .NET 和 JVM 都有着 GC(Garbage Collection), 即垃圾回收机制. 有一个个步骤来回收这些没用的对象, 到最终, 这些不被引用的托管对象(即被运行时管理的对象)都会被销毁.&lt;/p&gt;
&lt;p&gt;还有一个好玩的例子:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int[] myArray = new int[] {1, 2, 3};
int[] another = myArray;
another[0] = 100;
Console.WriteLine(myArray[0]);       // 结果是100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其原理就是, myArray 为最开始创建的数组的对象引用, 我们又将这个引用赋值给 another, 那么 another 跟 myArray 的值一样, 指向我们最开始创建的对象. 即, 通过 another来对数组更改, 其实跟通过 myArray 来更改无异. 因为它们表示同一个对象.&lt;/p&gt;
&lt;h2&gt;13. 非托管对象&lt;/h2&gt;
&lt;p&gt;注意, 对象的内存是非常大的, 所以如果不好好管理, 很容易造成内存泄漏. 但对于 .NET 和 Java, 这个问题小了一点, 因为大部分类型都可以被 GC 给清理掉.&lt;/p&gt;
&lt;p&gt;但是, 还是存在一些非托管对象的, 例如用户自己分配的内存, 以及 MemoryStream 之类的, 这些内存不会被 GC 清理掉, 用户需要自己管理, 一般这种包含非托管内存的类都会继承 IDisposable 接口, 用户可以调用它们的 Dispose 方法来清理掉这个对象的非托管内存.&lt;/p&gt;
&lt;h1&gt;2. 一些基本认知:&lt;/h1&gt;
&lt;h2&gt;1. 静态与非静态成员访问:&lt;/h2&gt;
&lt;h3&gt;1. 非静态成员可直接访问静态成员:&lt;/h3&gt;
&lt;p&gt;这个的话, 稍微想想就能理解. 例如一个厂子里刚造出一辆车, 这个车一看就知道是哪个牌子的, 所以可以轻易的找到这个厂子, 也就是说使用这个厂子里的零配件也是非常简单的, 即非静态成员(实例的成员)可直接访问静态成员(类的成员).&lt;/p&gt;
&lt;h3&gt;2. 静态成员无法直接访问为静态成员:&lt;/h3&gt;
&lt;p&gt;同样是汽车的例子, 这个车已经离开厂子了, 如果厂子要查看这个车的一些零配件损坏程度, 肯定是不大方便的, 因为你不知道这个车在哪, 即静态成员(类的成员)无法直接访问非静态成员(实例成员).&lt;/p&gt;
&lt;h3&gt;3. 通过对象引用来访问非静态成员:&lt;/h3&gt;
&lt;p&gt;静态成员来访问非静态成员, 肯定有办法访问, 例如这辆车在离开厂子前留下个联系方式, 这样厂子就可以找到这辆车. 下面的代码片是一个例子, 必须有对象引用才可以对其操作.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class TestObj
{
    static List&amp;lt;TestObj&amp;gt; instances = new List&amp;lt;TestObj&amp;gt;();
    private SelfValue = 0;
    public TestObj()
    {
        instances.Add(this);
    }
    public int Value =&amp;gt; SelfValue;
    public static ResetAllInstanceValue()
    {
        foreach (var ins in instances)
            instances.SelfValue = 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;喏, 这就是静态方法操作实例成员的示例. 只要你有这个对象的引用, 就可以对这个对象进行操作&lt;/p&gt;
</content:encoded></item><item><title>[Web前端] 子元素设置高度为100%, 却没有与父元素对齐高度.</title><link>https://slimenull.com/posts/20210307093339/</link><guid isPermaLink="true">https://slimenull.com/posts/20210307093339/</guid><description>大概描述一下我遇到的情况.父元素没有明确指定高度, 但是其中一个子元素的高度是确定的, 并且通过这个高度将父元素的高度撑起来. 另一个子元素的高度是100%, 即, 我想要使它与父元素的高度统一.但是, 指定高度100%的子元素没有与父元素的高度统一.不过, 如果明确而制定了父元素的高度为某一确定值, 那么这个指定高度100%的子元素的高度就生效了.奇怪的玄学问题增加了有几种解决方式供参考:使用绝对定位, 设置top和bottom为0使用表格布局, 这个我没用样式做过, 属性可以轻易做到</description><pubDate>Sun, 07 Mar 2021 09:33:39 GMT</pubDate><content:encoded>&lt;p&gt;大概描述一下我遇到的情况.&lt;/p&gt;
&lt;p&gt;父元素没有明确指定高度, 但是其中一个子元素的高度是确定的, 并且通过这个高度将父元素的高度撑起来. 另一个子元素的高度是100%, 即, 我想要使它与父元素的高度统一.&lt;/p&gt;
&lt;p&gt;但是, 指定高度100%的子元素没有与父元素的高度统一.&lt;/p&gt;
&lt;p&gt;不过, 如果明确而制定了父元素的高度为某一确定值, 那么这个指定高度100%的子元素的高度就生效了.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;奇怪的玄学问题增加了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有几种解决方式供参考:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用绝对定位, 设置top和bottom为0&lt;/li&gt;
&lt;li&gt;使用表格布局, 直接指定display为table&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>[Web前端] Margin 失效问题, 设置了 Margin 却不见效果, 解决方案.</title><link>https://slimenull.com/posts/20210307092631/</link><guid isPermaLink="true">https://slimenull.com/posts/20210307092631/</guid><description>首先演示下:.box1{    width: 300px;    height: 300px;}.box2{    width: 200px;    height: 200px;}.box3{    width: 100px;    height: 100px;}.border-with{    border: solid 1px pink;}.padding-with{    padding: 1px;}.style-pink{    background-colo</description><pubDate>Sun, 07 Mar 2021 09:26:31 GMT</pubDate><content:encoded>&lt;p&gt;首先演示下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box1{
    width: 300px;
    height: 300px;
}
.box2{
    width: 200px;
    height: 200px;
}
.border-with{
    border: solid 1px purple;
}
.padding-with{
    padding: 1px;
}
.style-pink{
    background-color: pink;
}
.style-green{
    background-color: green;
}
.margin-able{
    margin: 10px;
}
.margin-fix{
    overflow: hidden;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;box1 style-pink&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;box1 style-pink margin-able&quot;&amp;gt;
    &amp;lt;div class=&quot;box2 style-green margin-able&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果:&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20210307090937101.png&quot;/&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;发现, 绿色盒子我们明明定义了margin, 而顶部的margin却没有起作用.&lt;/p&gt;
&lt;p&gt;审查元素, 发现是这个情况:&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20210307091331513.png&quot;/&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;并且发现, 如果指定padding或border, 那么它们就会变得正常.&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20210307091619161.png&quot;/&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;div align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20210307091836101.png&quot;/&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;最好的修复方式是这样, 为父元素指定overflow为hidden:&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=&quot;center&quot;&amp;gt;&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20210307092049552.png&quot;/&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;可以给出一个我猜测的结论. margin是只是指与其他元素的间距, 即便它叫外边距. 因为父元素已经与上面的元素间距为5, 并且子元素没办法与别的元素产生边距, 所以就相对于最上方的元素计算边距, 即相对于父元素的间距是0. 而如果我们指定了border或padding, 他就会相对这多出的控件来运算边距, 即此时的margin相对于父元素.&lt;/p&gt;
&lt;p&gt;另外overflow也是一个奇妙的样式, 它还可以用来修复子元素float引起的高度塌陷问题.&lt;/p&gt;
</content:encoded></item><item><title>[C#] 好耶! 是幻影坦克! 原理讲解, 完整类库, 现成代码.</title><link>https://slimenull.com/posts/20210303164409/</link><guid isPermaLink="true">https://slimenull.com/posts/20210303164409/</guid><description>啥是幻影坦克? 幻影坦克就是, 一张黑白图片, 在黑色背景下和白色背景下能够显示出不同的图像.首先, 我可以明确的告诉你, 它的原理就是控制像素的颜色和Alpha通道(不透明度), 来使显示的图像在不同背景下显示不同的颜色.最基本的, 就是, 一张半透明的黑色薄膜, 如果在黑色的纸上, 你什么也看不出来, 但如果在白色纸上, 你可以看见, 它是灰色.本文中, Alpha 统一拟定为 0 ~ 1 的浮点数, 像素亮度统一为 0 ~ 1 即像素’白的程度’, 例如纯白为1, 纯黑则0.注意:本项</description><pubDate>Wed, 03 Mar 2021 16:44:09 GMT</pubDate><content:encoded>&lt;p&gt;啥是幻影坦克? 幻影坦克就是, 一张黑白图片, 在黑色背景下和白色背景下能够显示出不同的图像.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210303103019196.png&quot; alt=&quot;&quot; /&gt;
首先, 我可以明确的告诉你, 它的原理就是控制像素的颜色和Alpha通道(不透明度), 来使显示的图像在不同背景下显示不同的颜色.&lt;/p&gt;
&lt;p&gt;最基本的, 就是, 一张半透明的黑色薄膜, 如果在黑色的纸上, 你什么也看不出来, 但如果在白色纸上, 你可以看见, 它是灰色.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文中, Alpha 统一拟定为 0 ~ 1 的浮点数, 像素亮度统一为 0 ~ 1 即像素&apos;白的程度&apos;, 例如纯白为1, 纯黑则0.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;注意:&lt;/h2&gt;
&lt;p&gt;本项目已经在 &lt;a href=&quot;https://github.com/SlimeNull/Null.PhantomTank&quot;&gt;github.com/SlimeNull/Null.PhantomTank&lt;/a&gt; 开源. 你可以直接 clone 下来以查看源码.&lt;/p&gt;
&lt;p&gt;幻影坦克合成库, NullLib.PhantomTank 已经发布到 &lt;a href=&quot;https://www.nuget.org/packages/NullLib.PhantomTank/&quot;&gt;nuget.org&lt;/a&gt;, 你可以直接在 VS 的 nuget 包管理器中直接安装本库&lt;/p&gt;
&lt;h2&gt;基本原理:&lt;/h2&gt;
&lt;p&gt;像素在某背景下最终显示出来的公式如下:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;设: 这个图像的背景亮度为 $bc$, 这个像素的亮度为 $pc$, 不透明度为 $pa$, 则最终显示的颜色 $oc$ 就是:
$$
oc = pc  * pa + bc * (1 - pa)
$$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;理解起来也简单, 还拿刚刚的例子来讲, 例如一个纯黑的半透明薄膜, 那他肯定:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;只有一半的黑色能显示出来, 即 $pc * pa$&lt;/li&gt;
&lt;li&gt;而背景色, 也有一半的颜色能够透过来. 即: $bc * (1 - pa)$&lt;/li&gt;
&lt;li&gt;总的颜色加起来, 也就是 $pc * pa + bc * (1 - pa)$&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;幻影坦克:&lt;/h2&gt;
&lt;p&gt;而当一个像素为白色背景时, 能够显示出一个特定的颜色 $x$, 当黑色背景时, 显示出 $y$, 也可得出一个公式,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设: 颜色 $x$ 的亮度为 $xc$, 颜色 $y$ 的亮度为 $yc$, 这个像素的亮度为 $zx$, 不透明度为 $za$, 则满足:
$$
\begin{cases}
xc = za \times zc + (1 - za) \
yc = za \times zc
\end{cases}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;稍微处理一下, 可得到下面的公式:
$$
\begin{cases}
xc = yc + 1 \times (1 - za) \
yc = xc - 1 \times (1 - za) \
za = -[(xc - yc) \div 1] + 1 \
\end{cases}
\ \downarrow \
\begin{cases}
xc = yc + 1 - za \
yc = xc + za - 1 \
za = yc - xc + 1 \
\end{cases}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于 $xc$, $yc$, $zc$, $za$ 都是小于等于1, 大于等于0的值, 所以:
$$
\begin{cases}
1 \geq xc \geq 0 \
1 \geq yc \geq 0 \
1  \geq za \geq 0 \
\end{cases}
\ \downarrow \
\begin{cases}
1 \geq yc + 1 - za \geq 0 \
1 \geq xc + za - 1 \geq 0 \
1 \geq yc - xc + 1 \geq 0 \
\end{cases}
\ \downarrow \
\begin{cases}
0 \geq yc - za \geq -1 \
2 \geq yc + za \geq 1 \
0 \geq yc - xc \geq -1 \
\end{cases}
\ \downarrow \
\begin{cases}
za \geq yc \
xc + za \geq 1 \
xc \geq yc
\end{cases}
\ \
_\text{最终得到的不等式, 则是我们的 x 和 y 需要满足的条件.} \\
_\text{只有 x 和 y 像素满足这些条件, za 和 zc 才有值}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最终我们需要的算式是这些:
$$
\begin{cases}
zc = yc \div za \
za = yc - xc + 1
\end{cases}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;而 ARGB 通道的值是 0 ~ 255, 所以需要进行转换一下:
$$
\begin{cases}
zc = yc \div (za \div 255) = yc \times 255 \div za \
za = yc - xc + 255
\end{cases}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;而刚刚我们需要满足的条件, 其中只有一个是我们真正需要进行处理的, 即:
$$
xc \geq yc
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;解决方案:&lt;/h2&gt;
&lt;p&gt;关于 $xc \geq yc$ 的条件, 很简单, $xc$ 与 $yc$ 的值是在 $0$ 到 $255$ 之内的, 那我们只需要将其压制到 $128$ 到 $255$ 之间, 将 $yc$ 压制到 $0$ 到 $127$ 之间, 即可解决. 然后就可以直接用我们得出的公式来运算了.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Color CalcPixel(Color x, Color y)
{
    int
        xc = (x.R + x.G + x.B) / 3,
        yc = (y.R + y.G + z.B) / 3;   // 获取亮度
    
    xc = (xc / 255f) * 127 + 128;
    yc = (yc / 255f) * 127;           // 压制颜色

    int
        za = yc - xc + 255,
        zc = za == 0 ? 0 : yc * 255 / za;           // 运算结果颜色

    return Color.FromArgb(ya, yc, yc, yc);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们还可以加点功能, 就是 x 与 y 的颜色占用比例, 例如, 刚刚的就是 1:1, 如果是 10 : 245, 则 x 占用 245 ~ 255, y 占用 0 ~ 245.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src1ColorRatio 为 x 的占用比例, 值域是0~1
Color CalcPixel(Color src1, Color src2, float src1ColorRatio = 0.5f)
{
    float src2ColorRatio = 1 - src1ColorRatio;   // 运算出 y 的占用比例

    int
        xc = (int)((src1.R + src1.G + src1.B) * src1ColorRatio / 3 + src2ColorRatio * 255 + 1),
        yc = (int)((src2.R + src2.G + src2.B) * src2ColorRatio / 3);

    int za = yc - xc + 255,
        zc = za == 0 ? 0 : (yc * 255 / za);

    return Color.FromArgb(za, zc, zc, zc);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;完整代码:&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;需要的库:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;System.Drawing 程序集或 System.Drawing.Common 包.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;LockBitmap 源码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

namespace Null.PhantomTank.Library
{
    public class LockBitmap : IDisposable
    {
        Bitmap source = null;
        IntPtr Iptr = IntPtr.Zero;
        BitmapData bitmapData = null;

        public byte[] Pixels { get; set; }
        public int Depth { get; private set; }
        public int Width { get; private set; }
        public int Height { get; private set; }
        public bool IsLocked { get; private set; }

        private Func&amp;lt;int, Color&amp;gt; colorGetter;
        private Action&amp;lt;int, Color&amp;gt; colorSetter;

        public LockBitmap(Bitmap source)
        {
            this.source = source;
            LockBits();
        }

        /// &amp;lt;summary&amp;gt;
        /// Lock bitmap data
        /// &amp;lt;/summary&amp;gt;
        public void LockBits()
        {
            try
            {
                // Get width and height of bitmap
                Width = source.Width;
                Height = source.Height;

                // get total locked pixels count
                //int PixelCount = Width * Height;

                // Create rectangle to lock
                System.Drawing.Rectangle rect = new System.Drawing.Rectangle(0, 0, Width, Height);

                // get source bitmap pixel format size
                Depth = System.Drawing.Bitmap.GetPixelFormatSize(source.PixelFormat);

                // Check if bpp (Bits Per Pixel) is 8, 24, or 32
                if (Depth == 8)
                {
                    colorGetter = (offset) =&amp;gt; Color.FromArgb(Pixels[offset], Pixels[offset], Pixels[offset]);
                    colorSetter = (offset, color) =&amp;gt;
                    {
                        Pixels[offset] = color.B;
                    };
                }
                else if (Depth == 24)
                {
                    colorGetter = (offset) =&amp;gt; Color.FromArgb(Pixels[offset + 2], Pixels[offset + 1], Pixels[offset]);
                    colorSetter = (offset, color) =&amp;gt;
                    {
                        Pixels[offset] = color.B;
                        Pixels[offset + 1] = color.G;
                        Pixels[offset + 2] = color.R;
                    };
                }
                else if (Depth == 32)
                {
                    colorGetter = (offset) =&amp;gt; Color.FromArgb(Pixels[offset + 3], Pixels[offset + 2], Pixels[offset + 1], Pixels[offset]);
                    colorSetter = (offset, color) =&amp;gt;
                    {
                        Pixels[offset] = color.B;
                        Pixels[offset + 1] = color.G;
                        Pixels[offset + 2] = color.R;
                        Pixels[offset + 3] = color.A;
                    };
                }
                else
                {
                    throw new ArgumentException(&quot;Only 8, 24 and 32 bpp images are supported.&quot;);
                }

                // Lock bitmap and return bitmap data
                bitmapData = source.LockBits(rect, ImageLockMode.ReadWrite,
                                             source.PixelFormat);

                // create byte array to copy pixel values
                int step = Depth / 8;
                Pixels = new byte[bitmapData.Stride * Height];
                Iptr = bitmapData.Scan0;

                IsLocked = true;

                // Copy data from pointer to array
                Marshal.Copy(Iptr, Pixels, 0, Pixels.Length);
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        /// &amp;lt;summary&amp;gt;
        /// Unlock bitmap data
        /// &amp;lt;/summary&amp;gt;
        public void UnlockBits()
        {
            try
            {
                // Copy data from byte array to pointer
                Marshal.Copy(Pixels, 0, Iptr, Pixels.Length);

                // Unlock bitmap data
                source.UnlockBits(bitmapData);

                IsLocked = false;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        /// &amp;lt;summary&amp;gt;
        /// Get the color of the specified pixel
        /// &amp;lt;/summary&amp;gt;
        /// &amp;lt;param name=&quot;x&quot;&amp;gt;&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;y&quot;&amp;gt;&amp;lt;/param&amp;gt;
        /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
        public Color GetPixel(int x, int y)
        {
            Color clr = Color.Empty;

            // Get color components count
            int cCount = Depth / 8;

            // Get start index of the specified pixel
            //int i = ((y * Width) + x) * cCount;
            int i = y * bitmapData.Stride + x * cCount;

            if (i &amp;gt; Pixels.Length - cCount)
                throw new IndexOutOfRangeException();

            // Get color by array index
            clr = colorGetter.Invoke(i);

            return clr;
        }

        /// &amp;lt;summary&amp;gt;
        /// Set the color of the specified pixel
        /// &amp;lt;/summary&amp;gt;
        /// &amp;lt;param name=&quot;x&quot;&amp;gt;&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;y&quot;&amp;gt;&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;color&quot;&amp;gt;&amp;lt;/param&amp;gt;
        public void SetPixel(int x, int y, Color color)
        {
            // Get color components count
            int cCount = Depth / 8;

            // Get start index of the specified pixel
            //int i = ((y * Width) + x) * cCount;
            int i = y * bitmapData.Stride + x * cCount;

            // Set color by array index and color object
            colorSetter.Invoke(i, color);
        }

        public bool IsValidCoordinate(int x, int y)
        {
            return x &amp;gt;= 0 &amp;amp;&amp;amp; x &amp;lt; this.Width &amp;amp;&amp;amp; y &amp;gt; 0 &amp;amp;&amp;amp; y &amp;lt; this.Height;
        }

        #region IDisposable Support
        private bool disposedValue = false; // 要检测冗余调用

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    // TODO: 释放托管状态(托管对象)。
                }

                // TODO: 释放未托管的资源(未托管的对象)并在以下内容中替代终结器。
                // TODO: 将大型字段设置为 null。
                UnlockBits();
                disposedValue = true;
            }
        }

        // TODO: 仅当以上 Dispose(bool disposing) 拥有用于释放未托管资源的代码时才替代终结器。
        // ~LockBitmap() {
        //   // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
        //   Dispose(false);
        // }

        // 添加此代码以正确实现可处置模式。
        void IDisposable.Dispose()
        {
            // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
            Dispose(true);
            // TODO: 如果在以上内容中替代了终结器，则取消注释以下行。
            // GC.SuppressFinalize(this);
        }
        #endregion
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;幻影坦克制作源码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using Null.PhantomTank.Library;

namespace Null.PhantomTank
{
    public static class PhantomTank
    {
        /// 像素运算方式 变量 : 源色: Xc Yc, 输出: Za Zc
        ///   其中: c表示Color, 即颜色亮度, a代表Alpha通道, 即不透明度
        ///   备注: 值全部为0~1的比值
        ///
        /// Expr: | Xc = Za * Zc + 1 - Za
        ///       | Yc = Za * Zc
        /// 
        /// Calc: | Xc = Yc + 1 - Za
        ///       | Yc = Xc + Za - 1
        ///       | Za = Yc - Xc + 1
        /// 
        /// Need: | 1 &amp;gt;= Yc + 1 - Za &amp;gt;= 0, 0 &amp;gt;= Yc - Za &amp;gt;= -1;      Then: | Za &amp;gt;= Yc
        ///       | 1 &amp;gt;= Xc + Za - 1 &amp;gt;= 0, 2 &amp;gt;= Xc + Za &amp;gt;= 1;       Then: | Xc + Za &amp;gt;= 1
        ///       | 1 &amp;gt;= Yc - Xc + 1 &amp;gt;= 0, 0 &amp;gt;= Yc - Xc &amp;gt;= -1;      Then: | Xc &amp;gt;= Yc
        /// 
        /// Root: | Zc = Yc / Za
        ///       | Za = Yc - Xc + 1
        ///       
        ///       Basic Root: | Zc = Yc / (Za / 255) = Yc * 255 / Za
        ///                   | Za = Yc - Xc + 255

        private static Color CalcPixel(Color src1, Color src2, float src1ColorRatio = 0.5f)
        {
            float src2ColorRatio = 1 - src1ColorRatio;

            int
                xc = (int)((src1.R + src1.G + src1.B) * src1ColorRatio / 3 + src2ColorRatio * 255),
                yc = (int)((src2.R + src2.G + src2.B) * src2ColorRatio / 3);

            int
                za = yc - xc + 255,
                zc = za == 0 ? 0 : (yc * 255 / za);

            return Color.FromArgb(za, zc, zc, zc);
        }
        private static float ConvertRatio(float ratio)
        {
            return ratio / (ratio + 1);
        }
        public const float DefaultRatio = 1;
        public static Bitmap ResizeBitmap(Bitmap src, Color bgColor, ResizeMode resize, int newWidth, int newHeight)
        {
            Size srcSize = src.Size;

            int srcWidth = srcSize.Width,
                srcHeight = srcSize.Height;

            Bitmap result = new Bitmap(newWidth, newHeight, src.PixelFormat);
            Graphics rstG = Graphics.FromImage(result);

            rstG.Clear(bgColor);

            Size destSize;
            Rectangle srcRect, destRect;

            switch(resize)
            {
                case ResizeMode.NoResize:
                    destRect = new Rectangle((newWidth - srcWidth) / 2, (newHeight - srcHeight) / 2, srcWidth, srcHeight);
                    rstG.DrawImageUnscaled(src, destRect);
                    break;
                case ResizeMode.Stretch:
                    srcRect = new Rectangle(0, 0, srcWidth, srcHeight);
                    destRect = new Rectangle(0, 0, newWidth, newHeight);
                    rstG.DrawImage(src, destRect, srcRect, GraphicsUnit.Pixel);
                    break;
                case ResizeMode.Uniform:
                    srcRect = new Rectangle(0, 0, srcWidth, srcHeight);
                    destSize = new Size(newWidth, srcHeight * newWidth / srcWidth);
                    if (destSize.Height &amp;gt; newHeight)
                        destSize = new Size(srcWidth * newHeight / srcHeight, newHeight);
                    destRect = new Rectangle(new Point((newWidth - destSize.Width) / 2, (newHeight - destSize.Height) / 2), destSize);
                    rstG.DrawImage(src, destRect, srcRect, GraphicsUnit.Pixel);
                    break;
                case ResizeMode.UniformToFill:
                    srcRect = new Rectangle(0, 0, srcWidth, srcHeight);
                    destSize = new Size(newWidth, srcHeight * newWidth / srcWidth);
                    if (destSize.Height &amp;lt; newHeight)
                        destSize = new Size(srcWidth * newHeight / srcHeight, newHeight);
                    destRect = new Rectangle(new Point((newWidth - destSize.Width) / 2, (newHeight - destSize.Height) / 2), destSize);
                    rstG.DrawImage(src, destRect, srcRect, GraphicsUnit.Pixel);
                    break;
            }

            return result;
        }

        /// &amp;lt;summary&amp;gt;
        /// 最基本的合成方法, 请保证图片尺寸是一致的
        /// &amp;lt;/summary&amp;gt;
        /// &amp;lt;param name=&quot;src1&quot;&amp;gt;在白底下可以看到的图片&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;src2&quot;&amp;gt;在黑底下可以看到的图片&amp;lt;/param&amp;gt;
        /// &amp;lt;returns&amp;gt;合并后的黑白图像&amp;lt;/returns&amp;gt;
        public static Bitmap BasicCombineBitmap(Bitmap src1, Bitmap src2, float colorRatio)
        {
            if (src1 == null || src2 == null)
                throw new ArgumentNullException();
            if (src1.Size != src2.Size)
                throw new ArgumentOutOfRangeException();

            Size srcSize = src1.Size;
            int width = srcSize.Width, height = srcSize.Height;

            Bitmap result = new Bitmap(width, height, src1.PixelFormat);
            result.SetResolution(src1.HorizontalResolution, src1.VerticalResolution);

            LockBitmap lbmp1 = new LockBitmap(src1);
            LockBitmap lbmp2 = new LockBitmap(src2);
            LockBitmap lrst = new LockBitmap(result);

            float whiteRatio = ConvertRatio(colorRatio);

            for (int i = 0; i &amp;lt; height; i++)
            {
                for (int j = 0; j &amp;lt; width; j++)
                {
                    Color
                        srcPixel1 = lbmp1.GetPixel(j, i),
                        srcPixel2 = lbmp2.GetPixel(j, i),
                        outPixel = CalcPixel(srcPixel1, srcPixel2, whiteRatio);

                    lrst.SetPixel(j, i, outPixel);
                }
            }

            lbmp1.UnlockBits();
            lbmp2.UnlockBits();
            lrst.UnlockBits();

            return result;
        }

        /// &amp;lt;summary&amp;gt;
        /// 转换图片
        /// &amp;lt;/summary&amp;gt;
        /// &amp;lt;param name=&quot;src&quot;&amp;gt;源图&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;tankType&quot;&amp;gt;坦克类型&amp;lt;/param&amp;gt;
        /// &amp;lt;returns&amp;gt;转换结果&amp;lt;/returns&amp;gt;
        public static Bitmap ConvertBitmap(Bitmap src, TankType tankType)
        {
            Size srcSize = src.Size;

            int
                srcWidth = srcSize.Width,
                srcHeight = srcSize.Height;

            Bitmap result = new Bitmap(srcWidth, srcHeight, src.PixelFormat);
            result.SetResolution(src.HorizontalResolution, src.VerticalResolution);

            LockBitmap lsrc = new LockBitmap(src);
            LockBitmap lrst = new LockBitmap(result);

            Func&amp;lt;int, Color&amp;gt; pixelCalcFunc;
            switch (tankType)
            {
                case TankType.AppearOnBlack:
                    pixelCalcFunc = (srcPixel) =&amp;gt; Color.FromArgb(srcPixel, 255, 255, 255);
                    break;
                case TankType.AppearOnWhite:
                    pixelCalcFunc = (srcPixel) =&amp;gt; Color.FromArgb(255 - srcPixel, 0, 0, 0);
                    break;
                default:
                    throw new InvalidOperationException(&quot;Not supported.&quot;);
            }

            for (int i = 0; i &amp;lt; srcHeight; i++)
            {
                for (int j = 0; j &amp;lt; srcWidth; j++)
                {
                    Color pixel = lsrc.GetPixel(j, i);
                    int light = (pixel.R + pixel.G + pixel.B) / 3;
                    lrst.SetPixel(j, i, pixelCalcFunc.Invoke(light));
                }
            }

            lsrc.UnlockBits();
            lrst.UnlockBits();

            return result;
        }
        public static Bitmap ConvertImage(Image src, TankType tankType)
        {
            Bitmap newSrc = new Bitmap(src);
            Bitmap result = ConvertBitmap(newSrc, tankType);

            newSrc.Dispose();
            return result;
        }

        public static Bitmap CombineBitmap(Bitmap src1, Bitmap src2, Color bgColor1, Color bgColor2, ResizeMode resize, float colorRatio)
        {
            Size
                src1Size = src1.Size,
                src2Size = src2.Size;

            int src1Width = src1Size.Width,
                src1Height = src1Size.Height,
                src2Width = src2Size.Width,
                src2Height = src2Size.Height;

            int
                maxWidth = src1Width &amp;gt; src2Width ? src1Width : src2Width,
                maxHeight = src1Height &amp;gt; src2Height ? src1Height : src2Height;

            Bitmap
                newSrc1 = new Bitmap(maxWidth, maxHeight, src1.PixelFormat),
                newSrc2 = new Bitmap(maxWidth, maxHeight, src2.PixelFormat);

            Graphics
                srcG1 = Graphics.FromImage(newSrc1),
                srcG2 = Graphics.FromImage(newSrc2);

            srcG1.Clear(bgColor1);
            srcG2.Clear(bgColor2);

            // 这里进行的是对图片的重新调整尺寸操作
            switch(resize)
            {
                case ResizeMode.NoResize:
                    srcG1.DrawImageUnscaled(src1, (maxWidth - src1Width) / 2, (maxHeight - src1Height) / 2);
                    srcG2.DrawImageUnscaled(src2, (maxWidth - src2Width) / 2, (maxHeight - src2Height) / 2);
                    break;
                case ResizeMode.Stretch:
                    srcG1.DrawImage(src1, new Rectangle(0, 0, maxWidth, maxHeight), new Rectangle(0, 0, src1Width, src1Height), GraphicsUnit.Pixel);
                    srcG2.DrawImage(src2, new Rectangle(0, 0, maxWidth, maxHeight), new Rectangle(0, 0, src2Width, src2Height), GraphicsUnit.Pixel);
                    break;
                case ResizeMode.Uniform:
                    Size
                        scaleSize1 = new Size(maxWidth, (int)(src1Height * ((float)maxWidth / src1Width))),
                        scaleSize2 = new Size(maxWidth, (int)(src2Height * ((float)maxWidth / src2Width)));
                    if (scaleSize1.Height &amp;gt; maxHeight)
                        scaleSize1 = new Size((int)(src1Width * ((float)maxHeight / src1Height)), maxHeight);
                    if (scaleSize2.Height &amp;gt; maxHeight)
                        scaleSize2 = new Size((int)(src2Width * ((float)maxHeight / src2Height)), maxHeight);
                    srcG1.DrawImage(src1, new Rectangle(new Point((maxWidth - scaleSize1.Width) / 2, (maxHeight - scaleSize1.Height) / 2), scaleSize1), new Rectangle(0, 0, src1Width, src1Height), GraphicsUnit.Pixel);
                    srcG2.DrawImage(src2, new Rectangle(new Point((maxWidth - scaleSize2.Width) / 2, (maxHeight - scaleSize2.Height) / 2), scaleSize2), new Rectangle(0, 0, src2Width, src2Height), GraphicsUnit.Pixel);
                    break;
                case ResizeMode.UniformToFill:
                    Size
                        scaleFillSize1 = new Size(maxWidth, (int)(src1Height * ((float)maxWidth / src1Width))),
                        scaleFillSize2 = new Size(maxWidth, (int)(src2Height * ((float)maxWidth / src2Width)));
                    if (scaleFillSize1.Height &amp;lt; maxHeight)
                        scaleFillSize1 = new Size((int)(src1Width * ((float)maxHeight / src1Height)), maxHeight);
                    if (scaleFillSize2.Height &amp;lt; maxHeight)
                        scaleFillSize2 = new Size((int)(src2Width * ((float)maxHeight / src2Height)), maxHeight);
                    srcG1.DrawImage(src1, new Rectangle(new Point((maxWidth - scaleFillSize1.Width) / 2, (maxHeight - scaleFillSize1.Height) / 2), scaleFillSize1), new Rectangle(0, 0, src1Width, src1Height), GraphicsUnit.Pixel);
                    srcG2.DrawImage(src2, new Rectangle(new Point((maxWidth - scaleFillSize2.Width) / 2, (maxHeight - scaleFillSize2.Height) / 2), scaleFillSize2), new Rectangle(0, 0, src2Width, src2Height), GraphicsUnit.Pixel);
                    break;
            }

            srcG1.Dispose();
            srcG2.Dispose();

            Bitmap result = BasicCombineBitmap(newSrc1, newSrc2, colorRatio);    // 调用最终合成方法
            newSrc1.Dispose();
            newSrc2.Dispose();

            return result;
        }
        public static Bitmap CombineBitmap(Bitmap src1, Bitmap src2, Color bgColor1, Color bgColor2, ResizeMode resize)
        {
            return CombineBitmap(src1, src2, Color.White, Color.Black, resize, DefaultRatio);
        }
        public static Bitmap CombineBitmap(Bitmap src1, Bitmap src2, Color bgColor1, Color bgColor2, float colorRatio)
        {
            return CombineBitmap(src1, src2, Color.White, Color.Black, ResizeMode.NoResize, DefaultRatio);
        }
        public static Bitmap CombineBitmap(Bitmap src1, Bitmap src2, ResizeMode resize, float colorRatio)
        {
            return CombineBitmap(src1, src2, Color.White, Color.Black, resize, colorRatio);
        }
        public static Bitmap CombineBitmap(Bitmap src1, Bitmap src2, ResizeMode resize)
        {
            return CombineBitmap(src1, src2, Color.White, Color.Black, resize, 0);
        }
        public static Bitmap CombineBitmap(Bitmap src1, Bitmap src2, float colorRatio)
        {
            return CombineBitmap(src1, src2, Color.White, Color.Black, ResizeMode.NoResize, colorRatio);
        }
        public static Bitmap CombineBitmap(Bitmap src1, Bitmap src2)
        {
            return CombineBitmap(src1, src2, Color.White, Color.Black, ResizeMode.NoResize, DefaultRatio);
        }

        public static Bitmap CombineImage(Image src1, Image src2, Color bgColor1, Color bgColor2, ResizeMode resize, float colorRatio)
        {
            Bitmap
                newSrc1 = new Bitmap(src1),
                newSrc2 = new Bitmap(src2);

            Bitmap result = CombineBitmap(newSrc1, newSrc2, bgColor1, bgColor2, resize, colorRatio);
            newSrc1.Dispose();
            newSrc2.Dispose();

            return result;
        }
        public static Bitmap CombineImage(Image src1, Image src2, Color bgColor1, Color bgColor2, ResizeMode resize)
        {
            return CombineImage(src1, src2, Color.White, Color.Black, resize, DefaultRatio);
        }
        public static Bitmap CombineImage(Image src1, Image src2, Color bgColor1, Color bgColor2, float colorRatio)
        {
            return CombineImage(src1, src2, Color.White, Color.Black, ResizeMode.NoResize, colorRatio);
        }
        public static Bitmap CombineImage(Image src1, Image src2, ResizeMode resize, float colorRatio)
        {
            return CombineImage(src1, src2, Color.White, Color.Black, resize, colorRatio);
        }
        public static Bitmap CombineImage(Image src1, Image src2, ResizeMode resize)
        {
            return CombineImage(src1, src2, Color.White, Color.Black, resize, DefaultRatio);
        }
        public static Bitmap CombineImage(Image src1, Image src2, float colorRatio)
        {
            return CombineImage(src1, src2, Color.White, Color.Black, ResizeMode.NoResize, colorRatio);
        }
        public static Bitmap CombineImage(Image src1, Image src2)
        {
            return CombineImage(src1, src2, Color.White, Color.Black, ResizeMode.NoResize, DefaultRatio);
        }
    }
    public enum ResizeMode
    {
        NoResize,
        Stretch,
        Uniform,
        UniformToFill,
    }
    public enum TankType
    {
        AppearOnBlack,
        AppearOnWhite,
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;$\downarrow$ 点个赞再走呗! $\downarrow$&lt;/h2&gt;
</content:encoded></item><item><title>[安利] 什么鬼? 居然还有人不会截图??? 还在用手机对着电脑屏幕拍照????? 各种截图方式大集结!</title><link>https://slimenull.com/posts/20210227214017/</link><guid isPermaLink="true">https://slimenull.com/posts/20210227214017/</guid><description>某天, 我在一群里看到了这副画面… 我的天哪!!! 他居然!!! 还在用手机拍照! 简直妄为技术人! 得, 我今天就全方面得介绍一波你在电脑中可以进行截图的方式吧!截图方式:首先啊, 截图还是有不同的存储方式的, 像是手机里, 一般都是保存为文件, 电脑上当然也支持这种方式, 当然最最最好用的! 还是截图到剪切板. (不知道剪切板的可以重开人生以谢罪了)而这种方式保存的图呢, 一般用于粘贴出来. 例如 QQ 微信 的消息输入框, 你可以直接粘贴位于剪切板中的内容. 像是PS, Word, MSPai.</description><pubDate>Sat, 27 Feb 2021 21:40:17 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/images/20210220202627560.png&quot; alt=&quot;&quot; /&gt;
某天, 我在一群里看到了这副画面... 我的天哪!!! 他居然!!! 还在用手机拍照! 简直妄为技术人! 得, 我今天就全方面得介绍一波你在电脑中可以进行截图的方式吧!&lt;/p&gt;
&lt;h2&gt;截图方式:&lt;/h2&gt;
&lt;p&gt;首先啊, 截图还是有不同的存储方式的, 像是手机里, 一般都是保存为文件, 电脑上当然也支持这种方式, 当然最最最好用的! 还是截图到剪切板. (不知道剪切板的可以重开人生以谢罪了)&lt;/p&gt;
&lt;p&gt;而这种方式保存的图呢, 一般用于粘贴出来. 例如 QQ 微信 的消息输入框, 你可以直接粘贴位于剪切板中的内容. 像是PS, Word, MSPaint(画图), 这些程序也是支持粘贴图片的! 当然当然, CSDN 的 Markdown 编辑器也是支持粘贴图片的, 自动上传图片到 CSDN, 挺好用的哈哈.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210220203834456.png&quot; alt=&quot;QQ输入框粘贴图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210220203909645.png&quot; alt=&quot;画图中粘贴图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;通用截图:&lt;/h2&gt;
&lt;h5&gt;PrtSc 以及与之相关的截图快捷键:&lt;/h5&gt;
&lt;p&gt;各个系统! 通用的截图快捷键!!! PrtSc 键! 全称是 Print Screen, 按下它的作用是, 将整块屏幕的内容截取, 然后存储在剪切板中. 还有 Alt + PrtSc, 它是将当前的活动窗口的内容截取, 存储到剪切板.&lt;/p&gt;
&lt;p&gt;例如这样, 就是截取整个屏幕的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210220204007362.png&quot; alt=&quot;截取全屏&quot; /&gt;
这样, 就是截取当前活动窗口的, 因为我现在正在使用浏览器, 所以就是浏览器的内容, 不包括底部任务栏:
&lt;img src=&quot;/images/20210220204118422.png&quot; alt=&quot;截取活动窗口&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Windows 截图工具:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;估计早就没人用了, 建议跳过.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1. SnippingTool: Windows 自带的截图工具. 中文名: 截图工具.&lt;/h4&gt;
&lt;h5&gt;简述:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Snipping Tool 是一个很老的工具了, 至少在 XP 就已经存在, 并且, 虽然在 Win10 已经有了更好的截图方式, 不过这个工具还是保留了下来.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;效果图:&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210220204808135.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;使用方式:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;Windows 全局搜索:
&lt;img src=&quot;/images/20210220204631303.png&quot; alt=&quot;搜索Snipping Tool&quot; /&gt;&lt;/li&gt;
&lt;li&gt;运行:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210220204708154.png&quot; alt=&quot;运行SnippingTool&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;支持功能:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;矩形截屏:
在屏幕上拖拽出一个矩形, 并将举行包含的内容保存到剪切板.
&lt;img src=&quot;/images/20210220213247402.gif&quot; alt=&quot;SnippingTool矩形截屏&quot; /&gt;&lt;/li&gt;
&lt;li&gt;自由截屏:
随便画出一个形状, 然后将形状包含的内容保存到剪切板. &lt;img src=&quot;/images/20210220213737697.gif&quot; alt=&quot;SnippingTool自由截屏演示&quot; /&gt;&lt;/li&gt;
&lt;li&gt;窗口截屏:
选择一个窗口, 然后将窗口的内容保存到剪切板.
&lt;img src=&quot;/images/20210221233659634.gif&quot; alt=&quot;SnippingTool窗口截屏&quot; /&gt;&lt;/li&gt;
&lt;li&gt;截屏编辑:
截屏成功后, 会直接进入编辑页面. 并且还支持保存为 JPG, PNG, MHT 文件格式.
&lt;img src=&quot;/images/20210221235614159.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Win10 截图快捷键:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;估计不是很多人会用这个.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1. Win + Shift + S&lt;/h4&gt;
&lt;h5&gt;简述:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;是 Windows10 新增的截图快捷键, 截图后直接存储到剪切板, 并且可以在通知栏对图像进行更多操作.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;效果图:&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;/images/2021022123122610.gif&quot; alt=&quot;Win10截屏演示&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;支持功能:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;矩形截屏:
拖拽一个矩形, 然后截取矩形内的内容保存到剪切板.
&lt;img src=&quot;/images/20210221231332921.gif&quot; alt=&quot;Win10矩形截屏&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自由截屏:
自由选择一个区域, 然后保存到剪切板.
&lt;img src=&quot;/images/20210221234430996.gif&quot; alt=&quot;Win10自由截屏&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;窗口截屏:
选择窗口截屏, 保存到剪切板.
&lt;img src=&quot;/images/2021022123482916.gif&quot; alt=&quot;Win10窗口截屏&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;截取全屏:
点击, 即截取全屏并保存到剪切板.
&lt;img src=&quot;/images/20210221235020564.gif&quot; alt=&quot;Win10截取全屏&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;截屏编辑:
截屏成功的项目(Item), 可以在通知中查看, 点击, 即进入对该图的编辑.
&lt;img src=&quot;/images/20210221235249178.gif&quot; alt=&quot;Win10截屏编辑&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;QQ &amp;amp; 微信:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;不用说, 肯定有人不知道 QQ 和微信还有截图功能!&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1. QQ: Ctrl + Alt + A:&lt;/h4&gt;
&lt;h5&gt;简述:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;QQ 的默认截屏快捷键是 Ctrl + Alt + A, 温馨提示, QQ 的截屏功能非常强大, 是前面两个方式无法比拟的.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;效果图:&lt;/h5&gt;
&lt;p&gt;按 Ctrl + Alt + A 进入截图,
&lt;img src=&quot;/images/2021022200001067.gif&quot; alt=&quot;QQ截图&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;支持功能:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;矩形截屏:
正如刚刚演示的, 直接拖拽即可截取矩形区域, 并存储到剪切板. &lt;img src=&quot;/images/2021022200001067.gif&quot; alt=&quot;QQ截图&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;置顶贴纸:
截取一个区域后, 先不要确认, 点击一个钉子图标, 即可将截取的区域置顶悬浮, 并且可拖拽. 双击可关闭.
&lt;img src=&quot;/images/20210222000602186.gif&quot; alt=&quot;QQ截图贴纸功能&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文本识别与翻译功能:
选择完一个区域后, 还支持文本识别和翻译功能.&lt;img src=&quot;/images/20210222001305999.gif&quot; alt=&quot;QQ文本识别与翻译&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编辑功能:
选择完矩形区域, 进入编辑模式, 下面有非常多的选项可供使用. 从左到右, 依次是这些:
&lt;img src=&quot;/images/20210222001035814.png&quot; alt=&quot;QQ截图编辑功能&quot; /&gt;
&lt;img src=&quot;/images/2021022200184782.png&quot; alt=&quot;QQ截图编辑功能&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2. 微信: Alt + A:&lt;/h4&gt;
&lt;h5&gt;简述:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;微信的默认截屏键是 Alt + A, 不过微信的截屏相比 QQ 来讲, 就弱很多了, 没多少功能, 没有置顶贴纸功能.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;效果图:&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210222002258815.gif&quot; alt=&quot;微信截图&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;支持功能:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;矩形截屏:
跟 QQ 一模一样, 就不放图了&lt;/li&gt;
&lt;li&gt;编辑功能:
弱化版的 QQ 截图编辑, 只有形状, 箭头, 马赛克, 文本识别, 别的都没有.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第三方工具:&lt;/h2&gt;
&lt;p&gt;好了! 重点来了! QQ 的截图是强大了, 但是有的时候你不联网, 没办法登陆 QQ, 可不是就不能用 QQ 的截图了?&lt;/p&gt;
&lt;h4&gt;1. Snipaste:&lt;/h4&gt;
&lt;h5&gt;简述:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;官方网站: &lt;a href=&quot;https://www.snipaste.com/&quot;&gt;www.snipaste.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;一个非常强大的截图工具! 完全能够替代 QQ, 相比较 QQ, 它没有文本识别和长截图功能. 但是它可以作为一个非常好用的取色器(虽然 QQ 也彳亍, 但是 Snipaste 更好用)&lt;/li&gt;
&lt;li&gt;在各个工具上, 比 QQ 要细致更多! 例如 Snipaste 的有模糊工具和马赛克工具这两种, 并且可以调整模糊程度; 矩形工具和椭圆工具中, Snipaste 可以还放置实心的形状, 而 QQ 仅仅支持单线边框的形状.&lt;/li&gt;
&lt;li&gt;支持截图时微调, 通过 WSAD 按键, 可以一个像素一个像素的移动鼠标光标, 通过箭头, 可以移动整个选框的位置, 也是逐像素.&lt;/li&gt;
&lt;li&gt;更方便的快捷键, Snipaste 支持通过快捷键来将最后一个截取的图片作为置顶贴纸放置出来, 这是 QQ 所不支持的.&lt;/li&gt;
&lt;li&gt;更厉害的贴纸, 如果你复制了一个十六进制颜色代码, RGB颜色码, 一段文本, 或者是 Excel 表格中的内容, 这时你将它贴在屏幕上, Snipaste 会为你展示所有你需要的信息.&lt;/li&gt;
&lt;li&gt;支持多种语言, 自动适配系统区域设置.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;效果图:&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210222002916433.gif&quot; alt=&quot;Snipaste截图&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;支持功能:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;矩形截屏:
Snipaste 的默认截屏快捷键是 F1, 使用方式上与 QQ 的截屏没有太大的差异.
&lt;img src=&quot;/images/20210222002916433.gif&quot; alt=&quot;Snipaste截图&quot; /&gt;&lt;/li&gt;
&lt;li&gt;置顶贴纸:
当你截屏完毕后, 可以直接通过 F3 来将这个图像贴在屏幕上, 同样可移动, 并且还支持鼠标滚轮调整大小. 双击可关闭.
&lt;img src=&quot;/images/20210222003947421.gif&quot; alt=&quot;Snipaste贴纸&quot; /&gt;&lt;/li&gt;
&lt;li&gt;更细致的贴纸:
对于文本, 颜色值, 表格内容, 贴纸都有进行处理. 非常完美的格式化!
&lt;img src=&quot;/images/20210222005024642.gif&quot; alt=&quot;Snipaste对文本的处理&quot; /&gt;
&lt;img src=&quot;/images/202102220042467.gif&quot; alt=&quot;Snipaste对颜色的贴纸处理&quot; /&gt;
&lt;img src=&quot;/images/20210222004616265.gif&quot; alt=&quot;Snipaste对表格的贴纸处理&quot; /&gt;&lt;/li&gt;
&lt;li&gt;显示/隐藏 所有贴纸:
Snipaste 还支持临时隐藏所有的贴纸, 稍后可再次显示出来. 快捷键: Shift + F3 &lt;img src=&quot;/images/20210222005447324.gif&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2. ShareX:&lt;/h4&gt;
&lt;h5&gt;简述:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;好了好了! 大佬要来了! ShareX 可谓是一个非常强大的程序, 截图, 取色器, 屏幕取色器, 屏幕录制, 屏幕录制(GIF), 图像上传(到图床), 视频转格式,&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;程序图:&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210222011734713.png&quot; alt=&quot;ShareX程序界面&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;支持的功能:&lt;/h5&gt;
&lt;p&gt;写这么多真的是累死我了... 反正看这个界面图也差不多理解了, 我就截几个图放出来好了. 但是! ShareX 是一个超级厉害的软件啊啊啊啊! 一定要试试!&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;截图
&lt;img src=&quot;/images/20210222011931593.png&quot; alt=&quot;ShareX屏幕截图&quot; /&gt;&lt;/li&gt;
&lt;li&gt;工具:
&lt;img src=&quot;/images/20210222012041987.png&quot; alt=&quot;ShareX工具&quot; /&gt;&lt;/li&gt;
&lt;li&gt;快捷键编辑:
&lt;img src=&quot;/images/2021022201215764.png&quot; alt=&quot;ShareX&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;最后说两句:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;贴纸功能是真的好用啊淦! 我写文章的时候, 直接把 ShareX 的程序界面截取, 然后贴在屏幕上, 就可以慢慢一个个介绍了!&lt;/li&gt;
&lt;li&gt;总的来说, 储了最开始那个老工具和Win10截屏快捷键, 我还是都挺推荐的! 平常使用我一直用的 Snipaste, 截图和贴纸最常用, 然后录制 GIF 的时候, 就使用 ShareX. 毕竟录制 GIF 无非在 QQ 里发一些奇妙内容, 而没有多少人愿意去用 MP4 格式. 因为 GIF 自动播放的. (尽管大多数情况, MP4比GIF小, 质量又好)&lt;/li&gt;
&lt;li&gt;然后!!! 我再吹一波 ShareX! ShareX 的 GIF 录制是真的厉害! 我完全不知道它是如何压缩的图片大小. 质量非常棒! 而且 ShareX 的 MP4 录制, 大小压缩的是真的彳亍! Data Rate 仅有 397 kbps 的情况下, 居然完全能够保证质量! (听朋友说是压缩算法, 计算运动轨迹什么的, 估计仅在屏幕单调内容录制的情况下效果好.)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[C#] 就让这张图片来揭露你的本性吧! 老绅士.</title><link>https://slimenull.com/posts/20210221230837/</link><guid isPermaLink="true">https://slimenull.com/posts/20210221230837/</guid><description>淦!看到上面那幅图了吗? 放大, 放大, 看到了吗? 说的就是你 (滑稽 </description><pubDate>Sun, 21 Feb 2021 23:08:37 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/images/20210212012512552.jpg&quot; alt=&quot;qwq&quot; /&gt;
看到上面那幅图了吗? 放大, 放大, 看到了吗? 说的就是你 .&lt;/p&gt;
&lt;h2&gt;淦!&lt;/h2&gt;
&lt;h4&gt;1. 正常的:&lt;/h4&gt;
&lt;p&gt;简单的, 就是最开始那张图片了, 看起来还彳亍, 对吧&lt;/p&gt;
&lt;p&gt;好吧, 这种东西, 实现起来也不难, 无非是在图片中写字. 写字你总会吧? 我直接贴 Graphics 元数据:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace System.Drawing
{
    //
    // 摘要:
    //     封装一个 GDI+ 绘图图面。 此类不能被继承。
    public sealed class Graphics : MarshalByRefObject, IDeviceContext, IDisposable
    {
        //
        // 摘要:
        //     在指定位置并且用指定的 System.Drawing.Brush 和 System.Drawing.Font 对象绘制指定的文本字符串。
        //
        // 参数:
        //   s:
        //     要绘制的字符串。
        //
        //   font:
        //     System.Drawing.Font，它定义字符串的文本格式。
        //
        //   brush:
        //     System.Drawing.Brush，它确定所绘制文本的颜色和纹理。
        //
        //   point:
        //     System.Drawing.PointF 结构，它指定所绘制文本的左上角。
        //
        // 异常:
        //   T:System.ArgumentNullException:
        //     brush 为 null。 或 - s 为 null。
        public void DrawString(string s, Font font, Brush brush, PointF point);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;懂了吧? 当然本程序可没那么简单嘿嘿! 我可是还实现了类似于蒙版的东西! (好吧也不是很难)&lt;/p&gt;
&lt;h5&gt;2. 难一点的:&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210220205921619.jpg&quot; alt=&quot;&quot; /&gt;
看出有什么区别没? 没错! 你发现, 背景变成单色, 而字变成彩色的了!!! 当然 PS 用剪切蒙版很容易实现. 那么, 用 C# 该如何实现呢?&lt;/p&gt;
&lt;p&gt;首先创建一个位图, 作为我们的临时空间, 背景是黑色, 然后用白色的笔在上面写字. 哦豁, 这不就是一张蒙版吗?&lt;/p&gt;
&lt;p&gt;我们的源图像跟这个蒙版图是一样大小的, 输出图像当然也同样大小, 然后遍历原图像的像素, 并且判断蒙版图像的颜色, 如果蒙版图像的颜色是白色, 则把这个像素拷贝到输出图像中. 于是于是! 就实现了画彩色的字! (好吧的确有点投机取巧的意味.)&lt;/p&gt;
&lt;h5&gt;3. 简单点的:&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210221230415739.jpg&quot; alt=&quot;&quot; /&gt;
哈哈哈, 每一个字, 都相当于原图的一个像素, 可不可以说是变相的字符画?&lt;/p&gt;
&lt;h5&gt;完整的内容介绍:&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;Null.CharImg: 字符图像混合工具

Null.CharImg [ -{CharWidth | CWidth} 字符占宽 ]
             [ -{CharHeight | CHeight} 字符占高 ]
             [ -{MaxWidth | MWidth} 最大宽度 ]
             [ -{MaxHeight | MHeight} 最大高度 ]
             [ -{FontSize | FSize} 字体大小 ]
             [ -{String | Str} 字符串内容 ]
             [ -{CorrectRate | CRate} 颜色矫正程度]
             [ -{BackgroundColor | BgColor} 背景颜色 ]
             [ -{FontFamily | Font} 字体 ]
             [ /{FontBold | Bold} ]
             [ /{Opposite | Oppo} ]
             [ /{AutoCorrect | Correct}]
             [ /{AutoBackground | AutoBg}]
             [ /{HighDefinition | HD}]
             -{Output | Out} 输出路径
             *源文件

Null.CharImg /{Help | H | ?} 显示本帮助内容

      其中参数的概述如下:
             字符占宽, 指图像中, 一个字符独占的宽度, 默认8
             字符占高, 字符独占的高度, 与字符占宽决定了字符的间距, 默认16
             最大宽度, 指输出图像中, 横向最多含多少个字符
             最大高度, 指输出图像中, 纵向最多含多少个字符
             字体大小, 指字体大小, 单位是像素, 默认16
             字符串内容, 要显示在图像中的字符, 将逐个显示
             颜色矫正程度, 指定了自动颜色矫正的程度, 值域是0~100, 默认100
             背景颜色, 表示图像背景颜色, 使用十六进制代码表示
             输出路径, 表示输出图像的保存路径
             字体, 表示图像中字符所使用的字体, 默认是Sans Serif
             FontBold, 粗体, 表示是否使用粗体
             Opposite, 使背景与前景颜色反转
             AutoCorrect, 自动矫正颜色
             AutoBackground, 自动判断背景色
             HighDefinition, 高分辨率模式

      关于自动矫正颜色:
             自动矫正颜色旨在尝试调节前景色的色彩, 以做到前景与背景结合时, 颜色
             趋近于原图, 如果背景颜色指定了亮度较高的颜色, 例如白色, 这个功能会
             启到很好的作用, 如果你认为颜色矫正有些严重, 可以使用 CorrectRate
             参数来指定自动调节的力度, 尝试调节为80, 甚至更低的值来适应.

      关于自动判断背景色
             当启动该功能时, 用户设置的颜色作废, 程序会自动计算整个原图像的平均色
             作为输出图像的背景色

      关于高分辨率模式:
             当启用高分辨率时, 图像中的一个字符不再单单是一种颜色, 而是使用彩色,
             以使输出图像更能趋近于原图的形状与颜色, 同时Opposite也是受支持的

      关于程序:
             作者: 诺尔, Null; 擅长:摸鱼
             Github仓库: https://github.com/SlimeNull/Null.CharImg
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果有啥感到迷惑的, 在下方留言噢~&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>[C#] 绘制函数图像. 可拖动, 可缩放, 可调整精度</title><link>https://slimenull.com/posts/20210219220053/</link><guid isPermaLink="true">https://slimenull.com/posts/20210219220053/</guid><description>欸嘿, 这就是程序图了, 通过鼠标拖拽可以移动, 鼠标滚轮可以缩放, 右下角还可以选择要绘制的函数. 项目仓库链接在文章末尾基本原理:Graphics 绘图, 不用我说了吧? 如果你不是很懂, 留言, 我会专门写一篇文章来介绍 Graphics.带入求值, 没啥难的. 线是一个个点连起来的, 也就是:然后, 标尺, 也是一个个线呗, 那个数字的话, 就是这个:填充小三角的话, 就是这个:关于优化:首先是计算问题, 保证仅仅计算需要显示的区域, 区域外的坐标不予以计算, 以节省资源.然.</description><pubDate>Fri, 19 Feb 2021 22:00:53 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/images/b50d551223eb9fdb405aa2f1058e3eee.gif&quot; alt=&quot;&quot; /&gt;
欸嘿, 这就是程序图了, 通过鼠标拖拽可以移动, 鼠标滚轮可以缩放, 右下角还可以选择要绘制的函数. 项目仓库链接在文章末尾&lt;/p&gt;
&lt;h2&gt;基本原理:&lt;/h2&gt;
&lt;p&gt;Graphics 绘图, 不用我说了吧? 如果你不是很懂, 留言, 我会专门写一篇文章来介绍 Graphics.&lt;/p&gt;
&lt;p&gt;带入求值, 没啥难的. 线是一个个点连起来的, 也就是:
&lt;img src=&quot;/images/20210219052221569.png&quot; alt=&quot;&quot; /&gt;
然后, 标尺, 也是一个个线呗, 那个数字的话, 就是这个:
&lt;img src=&quot;/images/20210219052448721.png&quot; alt=&quot;&quot; /&gt;
填充小三角的话, 就是这个:
&lt;img src=&quot;/images/20210219052539913.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;关于优化:&lt;/h2&gt;
&lt;p&gt;首先是计算问题, 保证仅仅计算需要显示的区域, 区域外的坐标不予以计算, 以节省资源.&lt;/p&gt;
&lt;p&gt;然后是闪屏问题, 使用BufferedGraphics, 既能解决闪屏问题, 又不会像Bitmap缓冲那样闪屏.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于绘图闪屏问题, 网上有很多解决方案, 最核心的, 无非是双缓冲, 也就是先将图绘制到缓冲区, 再将缓冲区的内容绘制到屏幕上.
 
实现双缓冲有两种方式, 一就是通过创建一个Bitmap, 将图画到Bitmap上, 然后画完之后, 再将Bitmap画到屏幕上(Graphics.DrawImage()), 缺点是会造成撕裂问题.
第二种方式是 BufferedGraphics, 这是一个比Bitmap更好用的东西, 不会造成撕裂.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;封装一下:&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;

namespace Null.FuncDraw
{
    public static class FuncDraw
    {
        /// &amp;lt;summary&amp;gt;
        /// 根据数字坐标获取像素位置
        /// &amp;lt;/summary&amp;gt;
        /// &amp;lt;param name=&quot;xCoord&quot;&amp;gt;x坐标&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;yCoord&quot;&amp;gt;y坐标&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;xOffset&quot;&amp;gt;原点位置&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;yOffset&quot;&amp;gt;原点位置&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;scale&quot;&amp;gt;缩放&amp;lt;/param&amp;gt;
        /// &amp;lt;returns&amp;gt;像素位置&amp;lt;/returns&amp;gt;
        public static Point GetPointFromCoords(double xCoord, double yCoord, int xOffset, int yOffset, double scale)
        {
            return new Point((int)(xCoord * scale + xOffset), (int)(-yCoord * scale + yOffset));
        }
        /// &amp;lt;summary&amp;gt;
        /// 
        /// &amp;lt;/summary&amp;gt;
        /// &amp;lt;param name=&quot;coords&quot;&amp;gt;数字坐标&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;offset&quot;&amp;gt;原点位置&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;scale&quot;&amp;gt;缩放&amp;lt;/param&amp;gt;
        /// &amp;lt;returns&amp;gt;像素位置&amp;lt;/returns&amp;gt;
        public static Point GetPointFromCoords(PointF coords, Point offset, double scale)
        {
            return new Point((int)(coords.X * scale + offset.X), (int)(-coords.Y * scale + offset.Y));
        }
        /// &amp;lt;summary&amp;gt;
        /// 根据像素位置获取数字坐标
        /// &amp;lt;/summary&amp;gt;
        /// &amp;lt;param name=&quot;x&quot;&amp;gt;水平位置&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;y&quot;&amp;gt;竖直位置&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;xOffset&quot;&amp;gt;原点位置&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;yOffset&quot;&amp;gt;原点位置&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;scale&quot;&amp;gt;缩放&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;xCoord&quot;&amp;gt;输出: 数字X坐标&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;yCoord&quot;&amp;gt;输出: 数字Y坐标&amp;lt;/param&amp;gt;
        public static void GetCoordsFromPoint(int x, int y, int xOffset, int yOffset, double scale, out double xCoord, out double yCoord)
        {
            xCoord = (x - xOffset) / scale;
            yCoord = -((y - yOffset) / scale);
            return;
        }
        /// &amp;lt;summary&amp;gt;
        /// 根据像素长度返回数字
        /// &amp;lt;/summary&amp;gt;
        /// &amp;lt;param name=&quot;length&quot;&amp;gt;像素长度&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;scale&quot;&amp;gt;缩放&amp;lt;/param&amp;gt;
        /// &amp;lt;returns&amp;gt;数字&amp;lt;/returns&amp;gt;
        public static double GetNumberFromPixel(int length, double scale)
        {
            return length / scale;
        }
        /// &amp;lt;summary&amp;gt;
        /// 根据数字来获取它距离原点的像素长度
        /// &amp;lt;/summary&amp;gt;
        /// &amp;lt;param name=&quot;number&quot;&amp;gt;数字&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;scale&quot;&amp;gt;缩放&amp;lt;/param&amp;gt;
        /// &amp;lt;returns&amp;gt;像素长度&amp;lt;/returns&amp;gt;
        public static double GetPixelFromNumber(int number, double scale)
        {
            return number * scale;
        }
        /// &amp;lt;summary&amp;gt;
        /// 画函数图像
        /// &amp;lt;/summary&amp;gt;
        /// &amp;lt;param name=&quot;func&quot;&amp;gt;要画的函数&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;inputs&quot;&amp;gt;X的取值&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;graphics&quot;&amp;gt;绘图Graphics&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;pen&quot;&amp;gt;画线所用的Pen&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;drawArea&quot;&amp;gt;画函数的区域&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;xOffset&quot;&amp;gt;原点的位置&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;yOffset&quot;&amp;gt;原点的位置&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;scale&quot;&amp;gt;缩放参数&amp;lt;/param&amp;gt;
        public static void DrawFunc(Func&amp;lt;double, double&amp;gt; func, IEnumerable&amp;lt;double&amp;gt; inputs, Graphics graphics, Pen pen, Rectangle drawArea, int xOffset, int yOffset, double scale)
        {
            double[] nums = inputs.ToArray();
            Point[] coords = new Point[nums.Length];

            int drawAreaLeft = drawArea.X,
                drawAreaRight = drawAreaLeft + drawArea.Width,
                drawAreaTop = drawArea.Y,
                drawAreaBottom = drawAreaTop + drawArea.Height;

            double y;
            for (int i = 0, len = nums.Length; i &amp;lt; len; i++)          // 最近刚知道要少用属性, 毕竟属性的实质是函数, 函数调用需要分配栈空间, 而且似乎还涉及到装箱拆箱, 所以这样效率高一些
            {
                y = func.Invoke(nums[i]);
                coords[i] = GetPointFromCoords(nums[i], y, xOffset, yOffset, scale);
            }

            bool point1xIn1,
                point1xIn2,
                point1yIn1,
                point1yIn2,
                point2xIn1,
                point2xIn2,
                point2yIn1,
                point2yIn2;

            for (int i = 1, len = nums.Length; i &amp;lt; len; i++)
            {
                Point point1 = coords[i - 1];
                Point point2 = coords[i];

                point1xIn1 = point1.X &amp;gt;= drawAreaLeft;
                point1xIn2 = point1.X &amp;lt;= drawAreaRight;
                point1yIn1 = point1.Y &amp;gt;= drawAreaTop;
                point1yIn2 = point1.Y &amp;lt;= drawAreaBottom;
                point2xIn1 = point2.X &amp;gt;= drawAreaLeft;
                point2xIn2 = point2.X &amp;lt;= drawAreaRight;
                point2yIn1 = point2.Y &amp;gt;= drawAreaTop;
                point2yIn2 = point2.Y &amp;lt;= drawAreaBottom;

                if ((!(point1xIn1 &amp;amp;&amp;amp; point2xIn1)) || (!(point1xIn2 &amp;amp;&amp;amp; point2xIn2)) || (!(point1yIn1 &amp;amp;&amp;amp; point2yIn1)) || (!(point1yIn2 &amp;amp;&amp;amp; point2yIn2)))
                    continue;

                if (!point1xIn1)
                    point1 = new Point(drawAreaLeft, (int)(point1.Y + (point2.Y - point1.Y) * ((double)drawAreaLeft - point1.X) / (point2.X - point1.X)));      // 这里是有一些奇妙优化的, 不要删去. 否则将导致某些函数无法绘制.
                else if (!point2xIn1)
                    point2 = new Point(drawAreaLeft, (int)(point2.Y + (point1.Y - point2.Y) * ((double)drawAreaLeft - point2.X) / (point1.X - point2.X)));
                if (!point1xIn2)
                    point1 = new Point(drawAreaRight, (int)(point2.Y + (point1.Y - point2.Y) * ((double)drawAreaRight - point2.X) / (point1.X - point2.X)));
                else if (!point2xIn2)
                    point2 = new Point(drawAreaRight, (int)(point1.Y + (point2.Y - point1.Y) * ((double)drawAreaRight - point1.X) / (point2.X - point1.X)));
                if (!point1yIn1) 
                    point1 = new Point((int)(point1.X + (point2.X - point1.X) * ((double)drawAreaTop - point1.Y) / (point2.Y - point1.Y)), drawAreaTop);
                else if (!point2yIn1)
                    point2 = new Point((int)(point2.X + (point1.X - point2.X) * ((double)drawAreaTop - point2.Y) / (point1.Y - point2.Y)), drawAreaTop);
                if (!point1yIn2)
                    point1 = new Point((int)(point2.X + (point1.X - point2.X) * ((double)drawAreaBottom - point2.Y) / (point1.Y - point2.Y)), drawAreaBottom);
                else if (!point2yIn2)
                    point2 = new Point((int)(point1.X + (point2.X - point1.X) * ((double)drawAreaBottom - point1.Y) / (point2.Y - point1.Y)), drawAreaBottom);

                graphics.DrawLine(pen, point1, point2);
            }
        }
        /// &amp;lt;summary&amp;gt;
        /// 画一个坐标轴
        /// &amp;lt;/summary&amp;gt;
        /// &amp;lt;param name=&quot;xNumbers&quot;&amp;gt;x轴要画的数&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;yNumbers&quot;&amp;gt;y轴要画的数&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;graphics&quot;&amp;gt;绘图Graphics&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;brush&quot;&amp;gt;填充坐标轴三角所使用的Brush&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;pen&quot;&amp;gt;画坐标轴所使用的Pen&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;font&quot;&amp;gt;画坐标数字所使用的Font&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;drawArea&quot;&amp;gt;画坐标轴的区域&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;xOffset&quot;&amp;gt;原点的位置&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;yOffset&quot;&amp;gt;远点的位置&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;barLength&quot;&amp;gt;坐标轴上小竖线的长度&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;scale&quot;&amp;gt;缩放参数&amp;lt;/param&amp;gt;
        /// &amp;lt;param name=&quot;text&quot;&amp;gt;是否绘出数字&amp;lt;/param&amp;gt;
        public static void DrawShaft(IEnumerable&amp;lt;double&amp;gt; xNumbers, IEnumerable&amp;lt;double&amp;gt; yNumbers, Graphics graphics, Brush brush, Pen pen, Font font, Rectangle drawArea, int xOffset, int yOffset, int barLength, double scale, bool text)
        {
            Action&amp;lt;Point, double&amp;gt; drawText;

            if (text)
                drawText = (point, num) =&amp;gt; graphics.DrawString(num.ToString(&quot;F2&quot;), font, brush, point);      // 使用委托, 那么接下来就不需要进行多余的对text的判断, 而直接Invoke就彳亍
            else
                drawText = (point, num) =&amp;gt; { };

            Point
                yTop = new Point(xOffset, drawArea.Y),
                yBottom = new Point(xOffset, drawArea.Y + drawArea.Height),       // 确认轴的位置
                xLeft = new Point(drawArea.X, yOffset),
                xRight = new Point(drawArea.X + drawArea.Width, yOffset);

            graphics.DrawLine(pen, yTop, yBottom);            // 画轴
            graphics.DrawLine(pen, xLeft, xRight);

            int triangleHeight = (int)(Math.Tan(Math.PI / 3) * barLength);        // 坐标轴末端小三角的高度
            Point[]
                triangle1 = new Point[] { yTop, new Point(yTop.X - barLength, yTop.Y + triangleHeight), new Point(yTop.X + barLength, yTop.Y + triangleHeight) },
                triangle2 = new Point[] { xRight, new Point(xRight.X - triangleHeight, xRight.Y + barLength), new Point(xRight.X - triangleHeight, xRight.Y - barLength) };
            graphics.FillPolygon(brush, triangle1);           // 画三角
            graphics.FillPolygon(brush, triangle2);

            foreach (double x in xNumbers)
            {
                Point numBase = GetPointFromCoords(x, 0, xOffset, yOffset, scale);
                Point numEnd = new Point(numBase.X, numBase.Y - barLength);
                graphics.DrawLine(pen, numBase, numEnd);      // 画x轴数
                drawText(numBase, x);
            }

            foreach (double y in yNumbers)
            {
                Point numBase = GetPointFromCoords(0, y, xOffset, yOffset, scale);
                Point numEnd = new Point(numBase.X + barLength, numBase.Y);
                graphics.DrawLine(pen, numBase, numEnd);      // 画y轴数
                drawText(numBase, y);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;注意事项:&lt;/h4&gt;
&lt;p&gt;&lt;s&gt;Graphics 在绘制某些超级远的东西时, 例如这个坐标时 (2147483647,2147483647), 总之就是非常大, 那可能会不能绘制, 报 Overflow 异常&lt;/s&gt; &lt;strong&gt;(已解决)&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;还有一些要用的内容:&lt;/h4&gt;
&lt;p&gt;CSDN 文章: &lt;a href=&quot;/posts/20210219070741/&quot;&gt;C# 中的 Range 函数&lt;/a&gt;
CSDN 文章: &lt;a href=&quot;/posts/20210219152833/&quot;&gt;C# IEnumerable 连接, 将迭代器串起来&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;更新记录:&lt;/h4&gt;
&lt;p&gt;最新版本已经支持自定义函数表达式&lt;/p&gt;
&lt;h2&gt;项目:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;项目仓库:&lt;/strong&gt; &lt;a href=&quot;https://github.com/SlimeNull/Null.FuncDraw&quot;&gt;https://github.com/SlimeNull/Null.FuncDraw&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[C#] IEnumerable拼接! 将枚举器串起来~</title><link>https://slimenull.com/posts/20210219152833/</link><guid isPermaLink="true">https://slimenull.com/posts/20210219152833/</guid><description>本来以为 IEnumerable 不能拼接, 就自己实现了一个, 结果发现 Linq 是提供了一个 Concat 函数的, 不过似乎是通过生成List的方式来实现? 反正我那个是异步的.用来做参考还是非常不辍滴, 速度的话, 4个10000拼接, 然后重复迭代10000次, 我写的是3590ms, Linq 的是2931ms, 不过两个其实都没有直接 ToList() 然后迭代要快, ToList(), 然后AddRange, 其实耗时只有九百多毫秒, 足足差了3倍左右.static void Coll</description><pubDate>Fri, 19 Feb 2021 15:28:33 GMT</pubDate><content:encoded>&lt;p&gt;本来以为 IEnumerable 不能拼接, 就自己实现了一个, 结果发现 Linq 是提供了一个 Concat 函数的, 不过似乎是通过生成List的方式来实现? 反正我那个是异步的.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System;
using System.Collections;
using System.Collections.Generic;

namespace NullLib.Iterator
{
    public static class NIterator
    {
        public static IEnumerable Concat(this IEnumerable iterator1, IEnumerable iterator2)
        {
            foreach (var i in iterator1)
                yield return i;
            foreach (var i in iterator2)
                yield return i;
        }
        public static IEnumerable&amp;lt;T&amp;gt; Concat&amp;lt;T&amp;gt;(this IEnumerable&amp;lt;T&amp;gt; iterator1, IEnumerable&amp;lt;T&amp;gt; iterator2)
        {
            foreach (var i in iterator1)
                yield return i;
            foreach (var i in iterator2)
                yield return i;
        }
        public static IEnumerable Concat(this IEnumerable iterator1, params IEnumerable[] iterator2)
        {
            foreach (var i in iterator1)
                yield return i;
            foreach (var i in iterator2)
                foreach (var j in i)
                    yield return j;
        }
        public static IEnumerable&amp;lt;T&amp;gt; Concat&amp;lt;T&amp;gt;(this IEnumerable&amp;lt;T&amp;gt; iterator1, params IEnumerable&amp;lt;T&amp;gt;[] iterator2)
        {
            foreach (var i in iterator1)
                yield return i;
            foreach (var i in iterator2)
                foreach (var j in i)
                    yield return j;
        }
        public static IEnumerable Concat(params IEnumerable[] iterators)
        {
            foreach (var i in iterators)
                foreach (var j in i)
                    yield return j;
        }
        public static IEnumerable&amp;lt;T&amp;gt; Concat&amp;lt;T&amp;gt;(params IEnumerable&amp;lt;T&amp;gt;[] iterators)
        {
            foreach (var i in iterators)
                foreach (var j in i)
                    yield return j;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C#] Range函数, 好用就完事儿了!</title><link>https://slimenull.com/posts/20210219070741/</link><guid isPermaLink="true">https://slimenull.com/posts/20210219070741/</guid><description>这么多重载, 完全够用了~, 返回值是 IEnumerable&lt;T&gt;using System;using System.Collections;using System.Collections.Generic;namespace Null.Library{    public static partial class Lib    {        public static IEnumerable&lt;int&gt; Range(int stop)        {  </description><pubDate>Fri, 19 Feb 2021 07:07:41 GMT</pubDate><content:encoded>&lt;p&gt;这么多重载, 完全够用了~, 返回值是 IEnumerable&amp;lt;T&amp;gt;
其中, Range函数是简单的循环, RangeEx加入了检测, 不会造成死循环, 且精度非常准确&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System;
using System.Collections.Generic;

namespace NullLib.Range
{
    public class NRange
    {
        public static IEnumerable&amp;lt;int&amp;gt; Range(int stop)
        {
            for (int i = 0; i &amp;lt; stop; i++)
                yield return i;
        }
        public static IEnumerable&amp;lt;int&amp;gt; Range(int start, int stop)
        {
            for (int i = start; i &amp;lt; stop; i++)
                yield return i;
        }
        public static IEnumerable&amp;lt;int&amp;gt; Range(int start, int stop, int step)
        {
            for (int i = start; i &amp;lt; stop; i += step)
                yield return i;
        }
        public static IEnumerable&amp;lt;float&amp;gt; Range(float stop)
        {
            for (int i = 0; i &amp;lt; stop; i++)
                yield return i;
        }
        public static IEnumerable&amp;lt;float&amp;gt; Range(float start, float stop)
        {
            for (float i = start; i &amp;lt; stop; i++)
                yield return i;
        }
        public static IEnumerable&amp;lt;float&amp;gt; Range(float start, float stop, float step)
        {
            for (float i = start; i &amp;lt; stop; i += step)
                yield return i;
        }
        public static IEnumerable&amp;lt;double&amp;gt; Range(double stop)
        {
            for (int i = 0; i &amp;lt; stop; i++)
                yield return i;
        }
        public static IEnumerable&amp;lt;double&amp;gt; Range(double start, double stop)
        {
            for (double i = start; i &amp;lt; stop; i++)
                yield return i;
        }
        public static IEnumerable&amp;lt;double&amp;gt; Range(double start, double stop, double step)
        {
            for (double i = start; i &amp;lt; stop; i += step)
                yield return i;
        }
        public static IEnumerable&amp;lt;int&amp;gt; RangeEx(int stop)
        {
            int start = 0, step = 1, current = start;
            step = step &amp;lt; 0 ? -step : step;
            if (start &amp;lt; stop)
                for (int i = 0; current &amp;lt; stop; i++, current = step * i + start)
                    yield return current;
            else
                for (int i = 0; current &amp;gt; stop; i--, current = step * i + start)
                    yield return current;
        }
        public static IEnumerable&amp;lt;int&amp;gt; RangeEx(int start, int stop)
        {
            int step = 1, current = start;
            step = step &amp;lt; 0 ? -step : step;
            if (start &amp;lt; stop)
                for (int i = 0; current &amp;lt; stop; i++, current = step * i + start)
                    yield return current;
            else
                for (int i = 0; current &amp;gt; stop; i--, current = step * i + start)
                    yield return current;
        }
        public static IEnumerable&amp;lt;int&amp;gt; RangeEx(int start, int stop, int step)
        {
            int current = start;
            step = step &amp;lt; 0 ? -step : step;
            if (start &amp;lt; stop)
                for (int i = 0; current &amp;lt; stop; i++, current = step * i + start)
                    yield return current;
            else
                for (int i = 0; current &amp;gt; stop; i--, current = step * i + start)
                    yield return current;
        }
        public static IEnumerable&amp;lt;float&amp;gt; RangeEx(float stop)
        {
            float start = 0, step = 1, current = start;
            step = step &amp;lt; 0 ? -step : step;
            if (start &amp;lt; stop)
                for (int i = 0; current &amp;lt; stop; i++, current = step * i + start)
                    yield return current;
            else
                for (int i = 0; current &amp;gt; stop; i--, current = step * i + start)
                    yield return current;
        }
        public static IEnumerable&amp;lt;float&amp;gt; RangeEx(float start, float stop)
        {
            float step = 1, current = start;
            step = step &amp;lt; 0 ? -step : step;
            if (start &amp;lt; stop)
                for (int i = 0; current &amp;lt; stop; i++, current = step * i + start)
                    yield return current;
            else
                for (int i = 0; current &amp;gt; stop; i--, current = step * i + start)
                    yield return current;
        }
        public static IEnumerable&amp;lt;float&amp;gt; RangeEx(float start, float stop, float step)
        {
            float current = start;
            step = step &amp;lt; 0 ? -step : step;
            if (start &amp;lt; stop)
                for (int i = 0; current &amp;lt; stop; i++, current = step * i + start)
                    yield return current;
            else
                for (int i = 0; current &amp;gt; stop; i--, current = step * i + start)
                    yield return current;
        }
        public static IEnumerable&amp;lt;double&amp;gt; RangeEx(double stop)
        {
            double start = 0, step = 1, current = start;
            step = step &amp;lt; 0 ? -step : step;
            if (start &amp;lt; stop)
                for (int i = 0; current &amp;lt; stop; i++, current = step * i + start)
                    yield return current;
            else
                for (int i = 0; current &amp;gt; stop; i--, current = step * i + start)
                    yield return current;
        }
        public static IEnumerable&amp;lt;double&amp;gt; RangeEx(double start, double stop)
        {
            double step = 1, current = start;
            step = step &amp;lt; 0 ? -step : step;
            if (start &amp;lt; stop)
                for (int i = 0; current &amp;lt; stop; i++, current = step * i + start)
                    yield return current;
            else
                for (int i = 0; current &amp;gt; stop; i--, current = step * i + start)
                    yield return current;
        }
        public static IEnumerable&amp;lt;double&amp;gt; RangeEx(double start, double stop, double step)
        {
            double current = start;
            step = step &amp;lt; 0 ? -step : step;
            if (start &amp;lt; stop)
                for (int i = 0; current &amp;lt; stop; i++, current = step * i + start)
                    yield return current;
            else
                for (int i = 0; current &amp;gt; stop; i--, current = step * i + start)
                    yield return current;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[.NET] WPF DoubleAnimation 动画, 一篇文章悟透!</title><link>https://slimenull.com/posts/20210215160730/</link><guid isPermaLink="true">https://slimenull.com/posts/20210215160730/</guid><description>完全理解 DoubleAnimation! 仅需这一篇文章.介绍首先, DoubleAnimation可以用来创建一些简单的动画, 这些动画仅限于使用Double值的属性, 例如控件的位置, 尺寸, 缩放, 旋转, 或者透明度变化等.一个DoubleAnimation实例, 应该至少指定3个值:From (值从哪里开始), To (值到哪里结束), Duration (值的变化经过多少时间)例如, 如果要创建一个 1s 内从值从 0 到 1 的动画, From 就是0, To 就是 1, Du</description><pubDate>Mon, 15 Feb 2021 16:07:30 GMT</pubDate><content:encoded>&lt;p&gt;完全理解 DoubleAnimation! 仅需这一篇文章.&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;首先, DoubleAnimation可以用来创建一些简单的动画, 这些动画仅限于使用Double值的属性, 例如控件的位置, 尺寸, 缩放, 旋转, 或者透明度变化等.
一个DoubleAnimation实例, 应该至少指定3个值:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;From (值从哪里开始), To (值到哪里结束), Duration (值的变化经过多少时间)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如, 如果要创建一个 1s 内从值从 0 到 1 的动画, From 就是0, To 就是 1, Duration就是 1s.&lt;/p&gt;
&lt;p&gt;启动动画, 需要调用将要执行这个动画的对象的 BeginAnimation 方法, 例如一个控件, 或者一个窗口, 参数包括 DependencyProperty(这个动画将对哪个属性执行), AnimationTimeline(执行哪个动画)&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;实例:&lt;/h2&gt;
&lt;p&gt;例如, 创建一个, 当点击按钮时, 按钮的高度增加 30 个像素.
&lt;img src=&quot;/images/20210215113736959.png&quot; alt=&quot;&quot; /&gt;
CS代码:
&lt;img src=&quot;/images/20210215114618291.png&quot; alt=&quot;&quot; /&gt;
然后, 看看效果吧:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/037995efd77dcb8f94c17c888d866b7b.gif&quot; alt=&quot;控件高度变化动画演示&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其实, DoubleAnimation还可以指定运动时, 加速在整个时间线中的占比以及减速在整个时间线中的占比.&lt;/p&gt;
&lt;p&gt;例如, 你想指定这个运动中, 前半部分是加速, 后半部分是匀速, 那么加速占比就是0.5, 减速占比就是0, 如下:
&lt;img src=&quot;/images/20210215115429879.png&quot; alt=&quot;&quot; /&gt;
然后, 看看运行效果吧:
&lt;img src=&quot;/images/dee4b57b838eef385c68bada3ce48b9c.gif&quot; alt=&quot;控件高度加速变化动画演示&quot; /&gt;
可以看出, 有加速的效果了, 或者, 再狠点, 我们指定全部是减速, 加速占比0, 减速占比1:
&lt;img src=&quot;/images/16c2fcc56e8cebcf57f04410b310a585.gif&quot; alt=&quot;控件高度减速变化动画演示&quot; /&gt;
果然减速看起来非常好看啊.&lt;/p&gt;
&lt;h2&gt;纯XAML使用:&lt;/h2&gt;
&lt;p&gt;DoubleAnimation是可以在纯 xaml 中声明并使用的. 有种东西叫做 Trigger, 在 xaml 中声明他, 然后可以合适的触发动画, 如下:
&lt;img src=&quot;/images/20210215122545416.png&quot; alt=&quot;&quot; /&gt;
然后, 运行效果的话, 除了不能不断增加高度, 其他的是一样的, 点一次, 就会从20到50, 第二次点击, 还是从20到50.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h2&gt;贴代码:&lt;/h2&gt;
&lt;p&gt;按钮事件:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void Button_Click(object sender, RoutedEventArgs e)
{

    DoubleAnimation animation = new DoubleAnimation(
        button1.ActualHeight,                       // From        从按钮的 高度 开始
        button1.ActualHeight + 30,                  // To          到按钮的 高度 + 30 结束
        new Duration(TimeSpan.FromSeconds(1)))      // Duration    间隔是 1s
    {
        AccelerationRatio = 0,       // 设置加速占比为一半, 即 0.5
        DecelerationRatio = 1,       // 设置减速占比为0, 其实这里可以省略, 因为默认是0
    };

    button1.BeginAnimation(
        Button.HeightProperty,       // DependencyProperty      动画是针对于高度的
        animation);                  // AnimationTimeline       指定刚刚创建好的动画
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;纯xaml实现:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Button Name=&quot;button1&quot; Content=&quot;Button&quot; HorizontalAlignment=&quot;Left&quot; Margin=&quot;343,182,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;75&quot; Height=&quot;20&quot;&amp;gt;
    &amp;lt;Button.Triggers&amp;gt;
        &amp;lt;EventTrigger RoutedEvent=&quot;Button.Click&quot;&amp;gt;
            &amp;lt;BeginStoryboard&amp;gt;
                &amp;lt;Storyboard&amp;gt;
                    &amp;lt;DoubleAnimation
                                Storyboard.TargetProperty=&quot;Height&quot;
                                From=&quot;20&quot; 
                                To=&quot;50&quot;
                                Duration=&quot;0:0:1&quot;
                                AccelerationRatio=&quot;0&quot;
                                DecelerationRatio=&quot;1&quot;/&amp;gt;
                &amp;lt;/Storyboard&amp;gt;
            &amp;lt;/BeginStoryboard&amp;gt;
        &amp;lt;/EventTrigger&amp;gt;
    &amp;lt;/Button.Triggers&amp;gt;
&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;小彩蛋~ 是窗口加载时的透明度变化动画欸嘿&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Window.Triggers&amp;gt;
    &amp;lt;EventTrigger RoutedEvent=&quot;Loaded&quot;&amp;gt;
        &amp;lt;BeginStoryboard &amp;gt;
            &amp;lt;Storyboard&amp;gt;
                &amp;lt;DoubleAnimation From=&quot;0&quot; To=&quot;1&quot; Duration=&quot;0:0:0.3&quot; Storyboard.TargetProperty=&quot;Opacity&quot;/&amp;gt;
            &amp;lt;/Storyboard&amp;gt;
        &amp;lt;/BeginStoryboard&amp;gt;
    &amp;lt;/EventTrigger&amp;gt;
&amp;lt;/Window.Triggers&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考文章:&lt;/h2&gt;
&lt;p&gt;以下文章对本文章的理解有帮助噢~
&lt;a href=&quot;/posts/20210215160730/&quot;&gt;[.NET] WPF XAML 原理, 节点与实例, 以及一些重要的零碎知识点.&lt;/a&gt;
&lt;a href=&quot;/posts/20210215112655/&quot;&gt;[.NET] WPF Triggers 触发器, EventTrigger, Trigger&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>[.NET] WPF Triggers 触发器, EventTrigger, Trigger</title><link>https://slimenull.com/posts/20210215112655/</link><guid isPermaLink="true">https://slimenull.com/posts/20210215112655/</guid><description>触发器, 如其名, 可根据某些事件或属性值来触发某些东西, 那么我们来了解一下 xaml 中的触发器吧.样式变更:我们试试通过使用普通触发器来实现当鼠标悬停时更改控件背景颜色.然后运行一下, 效果就是这样:启动动画:然后, 试着用事件触发器(EventTrigger)与DoubleAnimation来创建一个启动动画吧(其实代码都写好了):然后运行效果如下:可以看到, 窗口是渐渐浮现的, 也就是Opacity从0到1.什么? 你问我对应的CS代码怎么写?先看看这篇文章吧, 看完它,</description><pubDate>Mon, 15 Feb 2021 11:26:55 GMT</pubDate><content:encoded>&lt;p&gt;触发器, 如其名, 可根据某些事件或属性值来触发某些东西, 那么我们来了解一下 xaml 中的触发器吧.&lt;/p&gt;
&lt;h2&gt;样式变更:&lt;/h2&gt;
&lt;p&gt;我们试试通过使用普通触发器来实现当鼠标悬停时更改控件背景颜色.
&lt;img src=&quot;/images/20210215073411292.png&quot; alt=&quot;&quot; /&gt;
然后运行一下, 效果就是这样:
&lt;img src=&quot;/images/066155f4f6d1a796a57440cae7ecc364.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;启动动画:&lt;/h2&gt;
&lt;p&gt;然后, 试着用事件触发器(EventTrigger)与DoubleAnimation来创建一个启动动画吧(其实代码都写好了):
&lt;img src=&quot;/images/2021021507435775.png&quot; alt=&quot;&quot; /&gt;
然后运行效果如下:
&lt;img src=&quot;/images/bf9d63b8401128a5fffe3d6e9324e951.gif&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
可以看到, 窗口是渐渐浮现的, 也就是Opacity从0到1.&lt;/p&gt;
&lt;h4&gt;什么? 你问我对应的CS代码怎么写?&lt;/h4&gt;
&lt;p&gt;先看看这篇文章吧, 看完它, 相信大多数的 xaml 代码, 你都能写出对应的 CS 代码, 这是一个通用的规律.
&lt;a href=&quot;/posts/20210215160730/&quot;&gt;[.NET] WPF XAML 原理, 节点与实例, 以及一些重要的零碎知识点.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;p&gt;推荐文章:
&lt;a href=&quot;/posts/20210215160730/&quot;&gt;[.NET] WPF DoubleAnimation 动画, 一篇文章悟透!&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>[.NET] WPF XAML 原理, 节点与实例, 以及一些重要的零碎知识点.</title><link>https://slimenull.com/posts/20210215112610/</link><guid isPermaLink="true">https://slimenull.com/posts/20210215112610/</guid><description>当你查资料时, 看到那些眼花缭乱的 xaml 代码, 让人瞬间没有了学习的欲望… 先试着寻找下其中的规律吧.节点与实例:首先看看上面的文章, 从我们最常接触的 Button 入手吧. Button 是可以用 CS 代码来进行实例化, 然后放置在窗口中的, 而其它的元素, 例如根节点 Window, 都是可以通过 CS 代码进行实例化.可以推测出, 在 xaml 中, 一个个的节点, 例如 Button, Label, 其实就是等同于声明一个对应类型的实例.那么如何在 CS 代码中访问这个实例呢? </description><pubDate>Mon, 15 Feb 2021 11:26:10 GMT</pubDate><content:encoded>&lt;p&gt;当你查资料时, 看到那些眼花缭乱的 xaml 代码, 让人瞬间没有了学习的欲望... 先试着寻找下其中的规律吧.&lt;/p&gt;
&lt;h2&gt;节点与实例:&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210215061732521.png&quot; alt=&quot;&quot; /&gt;
首先看看上面的文章, 从我们最常接触的 Button 入手吧. Button 是可以用 CS 代码来进行实例化, 然后放置在窗口中的, 而其它的元素, 例如根节点 Window, 都是可以通过 CS 代码进行实例化.&lt;/p&gt;
&lt;p&gt;可以推测出, 在 xaml 中, 一个个的节点, 例如 Button, Label, 其实就是等同于声明一个对应类型的实例.&lt;/p&gt;
&lt;p&gt;那么如何在 CS 代码中访问这个实例呢? 很简单, 为这个实例指定一个名称(Name), 就如上面的图片中为 Label 指定 Name 那样. 然后就可以在 CS 代码中访问了
&lt;img src=&quot;/images/20210215062326997.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;实例的属性:&lt;/h2&gt;
&lt;p&gt;如果要设置一个 Label 的 Content, 在 xaml 代码中, 该如何设置? 最常用的肯定是下面这个:
&lt;img src=&quot;/images/20210215062553167.png&quot; alt=&quot;&quot; /&gt;
xaml 还支持一种, 就是这样:
&lt;img src=&quot;/images/20210215062744751.png&quot; alt=&quot;&quot; /&gt;
其实 TextBox 的 Text 属性也是可以这么设置的:
&lt;img src=&quot;/images/20210215062905536.png&quot; alt=&quot;&quot; /&gt;
大概猜出了吧? xaml中, 一堆标签之间的值可以设置为对应实例的特定属性设置值. 例如 Label 就是设置Content, TextBox 就是 设置 TextBox.&lt;/p&gt;
&lt;p&gt;其实, 还有另外一种, 你肯定见过啦, Grid.RowDefinitions 就是这么用的.
&lt;img src=&quot;/images/20210215063244895.png&quot; alt=&quot;&quot; /&gt;
你可能会想到, Label 和 TextBox 这类普通的控件, 是否也支持呢?
&lt;img src=&quot;/images/20210215063437536.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;哈哈, 很有趣吧, 所以说, 以后看到什么 xaml 代码, 对应的 CS 代码也能猜出来一些, 毕竟如果是控件, 也无非是设置属性以及添加成员了, 噢, 下面就是添加成员咯.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;声明控件成员:&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210215064400870.png&quot; alt=&quot;&quot; /&gt;
如果一个控件可以有子成员, 就可以像上面那样声明, 当然, 这个也肯定经常用, 只是可能没注意过罢了.&lt;/p&gt;
&lt;h2&gt;自定义类型实例:&lt;/h2&gt;
&lt;p&gt;少年, 学过数据绑定吗? 数据绑定的上下文如何声明的, 应该还记得吧. 我之前就像一个憨憨一样, 去用 CS 代码来指定 DataContext. (文章末有关于数据绑定的&lt;a href=&quot;#end_wpfdatabinding&quot;&gt;文章推荐&lt;/a&gt;)
&lt;img src=&quot;/images/20210215065018565.png&quot; alt=&quot;&quot; /&gt;
但是,,, 刚刚我们说过, 想 Button, Label 这样的 UI 组件, 其实在声明后, 其实就是一个个的实例, 我们刚刚进行的指定上下文操作, 也是创建一个 TestModel 实例, 然后指定给这个窗口的 DataContext 属性.&lt;/p&gt;
&lt;p&gt;如何指定属性, 我们知道, 那如何在 xaml 中创建我们自己定义的类型实例呢? 下面是一个示例:
&lt;img src=&quot;/images/20210215065718402.png&quot; alt=&quot;&quot; /&gt;
你悟了吗? 通过这个, 我们就可以在 xaml 中声明实例了... 当然, 肯定有小聪明在想, &quot;我试试在 xaml 里面声明一个浮点数试试?&quot;
&lt;img src=&quot;/images/20210215070612804.png&quot; alt=&quot;&quot; /&gt;
没错, 那个憨憨就是我, 亲自尝试了一下, 不过也不算是没有收获, 至少我们可以知道, 这些实例的声明, 肯定是在特定的情况下才是可以的. 例如这个 Grid 子元素, 你必须指定一个 UIElement, 这跟 CS 代码里的一模一样hhh&lt;/p&gt;
&lt;p&gt;所以 xaml 跟 CS 还是相关联的哈哈哈, 我一开始还觉得 xaml 跟 CS 的关联不是很大呢.&lt;/p&gt;
&lt;h2&gt;查看窗体完整代码:&lt;/h2&gt;
&lt;p&gt;如果你用过 WinForm, 肯定会经常接触这个, 当指定了控件的某些事件, 而这个函数又不存在的时候肯定需要自己调整代码的:
&lt;img src=&quot;/images/2021021507141041.png&quot; alt=&quot;&quot; /&gt;
WinForm 的窗体完整代码就在这里了, 一到WPF, 就令人窒息了, 因为没有直接显示这个文件.&lt;/p&gt;
&lt;p&gt;不过, 你还是可以通过这样的方式去查看的:
&lt;img src=&quot;/images/20210215071805683.png&quot; alt=&quot;&quot; /&gt;
然后, 欢呼吧, 虽然一般很少动这个文件(你也没办法动, 自动生成会覆盖你的改动), 但是看这个东西, 肯定对学习 WPF 有帮助的.
&lt;img src=&quot;/images/20210215071852423.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;/images/202102150720390.png&quot; alt=&quot;&quot; /&gt;
&amp;lt;span id=&quot;windowclass&quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;/p&gt;
&lt;h2&gt;窗体类型:&lt;/h2&gt;
&lt;p&gt;其实这里只是指 xaml 中, Window 节点的 x:Class 属性, 我们知道, WPF 会有一些自动生成的代码, 这些代码中声明了这个窗体, 而这个生成的代码中窗体的命名空间和类名, 就是通过 xaml 文档中的 x:Class 属性指定的.&lt;/p&gt;
&lt;p&gt;例如我指定 x:Class=&quot;MvvmTest.View.MainWindow&quot;, 那么完整代码中就是这样的:
&lt;img src=&quot;/images/20210215175526504.png&quot; alt=&quot;&quot; /&gt;
所以说, 如果要改变这个窗体的命名空间, 你需要指定 x:Class 属性, 以及指定 xaml.cs 文件的命名空间.&lt;/p&gt;
&lt;p&gt;例如, 你将一个窗体从项目的根目录移动到了项目的View文件夹, 此时, 规范的做法应该是同时将这个窗体的命名空间添加上一个View.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;p&gt;看的这么开心, 赶快关注诺尔吧qwq, 别忘了点个赞噢~&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;p&gt;推荐文章:
&lt;a href=&quot;/posts/20210211102111/&quot;&gt;[C#] MCI 详解与封装类, MCI 播放音乐, 获取播放状态, 获取音频长度, 进度调整&lt;/a&gt;
&lt;a href=&quot;/posts/20230902090054/&quot;&gt;[.NET] WPF 数据绑定与 MVVM 模型&lt;/a&gt;&amp;lt;span id=&quot;end_wpfdatabinding&quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>[C#/VB.NET] C# 与 VB.NET 的类库跨语言引用.</title><link>https://slimenull.com/posts/20210215015702/</link><guid isPermaLink="true">https://slimenull.com/posts/20210215015702/</guid><description>例如你有一个程序, 是 VB.NET 写的, 你希望在里面引用 C# 的代码, 那么, 该如何操作呢?准备材料:一个 C# 或 VB.NET 项目, 我们以一个测试项目(WinForm)开始与该项目语言不同的, 但是是基于 .NET 的类代码片. 代码地址: 一个类实现控件拖拽移动分布演示:1. &gt; 创建类库项目:创建一个与我们准备的类库语言相同的类库, 并将代码拷贝到其中:2. &gt; 添加项目引用:在刚刚准备好的 WinForm 项目中, 添加对类库项目的引用.</description><pubDate>Mon, 15 Feb 2021 01:57:02 GMT</pubDate><content:encoded>&lt;p&gt;例如你有一个程序, 是 VB.NET 写的, 你希望在里面引用 C# 的代码, 那么, 该如何操作呢?&lt;/p&gt;
&lt;h2&gt;准备材料:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;一个 C# 或 VB.NET 项目, 我们以一个测试项目(WinForm)开始&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210215012757638.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;与该项目语言不同的, 但是是基于 .NET 的类代码片. 代码地址: &lt;a href=&quot;/posts/20200706065250/&quot;&gt;一个类实现控件拖拽移动&lt;/a&gt;
&lt;img src=&quot;/images/20210215012826547.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;分布演示:&lt;/h2&gt;
&lt;h4&gt;1. &amp;gt; 创建类库项目:&lt;/h4&gt;
&lt;p&gt;创建一个与我们准备的类库语言相同的类库, 并将代码拷贝到其中:
&lt;img src=&quot;/images/20210215013356542.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;2. &amp;gt; 添加项目引用:&lt;/h4&gt;
&lt;p&gt;在刚刚准备好的 WinForm 项目中, 添加对类库项目的引用.
&lt;img src=&quot;/images/20210215013532277.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;/images/20210215013627662.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;3. &amp;gt; 开始使用:&lt;/h4&gt;
&lt;p&gt;现在, 你就可以直接使用类库中的类了.
&lt;img src=&quot;/images/20210215014935805.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;&amp;gt; 使用效果:&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/images/5fd315dfb8b8e8c224db58c96f7797f5.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>[C#] 音乐播放 3 种方式 Demo 与 MCI 音乐播放器封装类.</title><link>https://slimenull.com/posts/20210211104239/</link><guid isPermaLink="true">https://slimenull.com/posts/20210211104239/</guid><description>淦!首先代码是不完整的, 我还是没能成功设置音量, 除此之外, 别的倒是挺顺利的, 当前实现的功能如下:播放 暂停 还原 停止 获取进度 获取长度 设置进度 获取播放状态内容:本项目的仓库在: SlimeNull/AudioTest, 在这里下载哦, 别忘了还原 nuget 包, 因为用了 NAudio 库.&gt; 音乐播放 Demo:先放图吧:首先左上角是打开文件的, 然后 PlayType 是播放方式, 我现在让它支持 3 种, 一种是SoundPlayer, 然后是 M</description><pubDate>Thu, 11 Feb 2021 10:42:39 GMT</pubDate><content:encoded>&lt;h2&gt;淦!&lt;/h2&gt;
&lt;p&gt;首先代码是不完整的, 我还是没能成功设置音量, 除此之外, 别的倒是挺顺利的, 当前实现的功能如下:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;播放 暂停 还原 停止 获取进度 获取长度 设置进度 获取播放状态&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;内容:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本项目的仓库在: &amp;lt;a id=&quot;download_link&quot; href=&quot;https://github.com/SlimeNull/AudioTest&quot;&amp;gt;SlimeNull/AudioTest&amp;lt;/a&amp;gt;, 在这里下载哦, 别忘了还原 nuget 包, 因为用了 NAudio 库.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&amp;gt; 音乐播放 Demo:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;先放图吧:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210211102257257.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;首先左上角是打开文件的, 然后 PlayType 是播放方式, 我现在让它支持 3 种, 一种是SoundPlayer, 然后是 MCI, 然后是 NAudio 库.
 
实现了播放, 暂停, 停止, 移除, 进度调整到开头, 进度调整(就那个track bar), 同步播放(如果点了这个, UI线程会阻塞的哦), 获取音乐长度, 获取播放状态, 获取播放位置, 重复播放.
 
不同的功能对不同的播放种类支持性不同, 所以, 如果你按下了按钮, 没反应, 也没报错, 那就是它不支持这个功能(也就是代码里没有写)&apos;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下载代码, 请看前面的链接.&lt;a href=&quot;#download_link&quot;&gt;↑跳转↑&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;&amp;gt; MCI 播放器封装:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;上边的窗体程序就体现了封装类所实现的所有功能喔, 所以试试吧, 你也可以直接粘贴下面的代码, 但是cs文件的名称, 如果可以的话, 我希望你命名为 &quot;Null.MciPlayer&quot; &amp;lt;sub&amp;gt;(没错就是我的名字&amp;lt;/sub&amp;gt;&lt;pre&gt;&lt;code&gt;using System;
using System.IO;
using System.Runtime.InteropServices;

namespace Null.MciPlayer
{
    public class MciPlayer : IDisposable
    {
        [DllImport(&quot;kernel32.dll&quot;, EntryPoint = &quot;GetShortPathNameW&quot;, CharSet = CharSet.Unicode)]
        extern static short GetShortPath(string longPath, string buffer, int bufferSize);
        [DllImport(&quot;winmm.dll&quot;, EntryPoint = &quot;mciSendString&quot;, CharSet = CharSet.Unicode)]
        extern static int MciSendString(string command, string buffer, int bufferSize, IntPtr callback);

        bool TryGetShortPath(string longPath, out string shortPath)
        {
            shortPath = null;
            short reqLen = GetShortPath(longPath, null, 0);   // 指定 null与0, 则返回需要的长度
            if (reqLen == 0)
                return false;

            shortPath = new string(&apos;\0&apos;, reqLen);   // 声明缓冲

            short rstLen = GetShortPath(longPath, shortPath, reqLen);   // 转换
            if (rstLen == 0 || rstLen == reqLen)
                return false;

            shortPath = shortPath.TrimEnd(&apos;\0&apos;);
            return true;
        }

        private string longpath;
        private string shortName;
        private string aliasName;
        public MciPlayer() { }
        public MciPlayer(string path)
        {
            if (!File.Exists(path))
                throw new FileNotFoundException(&quot;File not exist.&quot;, path);

            longpath = path;
        }
        void MciSendStringWithCheck(string command, string buffer, int bufferSize, IntPtr callback)
        {
            int err = MciSendString(command, buffer, bufferSize, callback);
            if (err != 0)
                throw new MciException(err);
        }
        string StatusInfo(string info)
        {
            string buffer = new string(&apos;\0&apos;, 32);
            MciSendStringWithCheck($&quot;status {aliasName} {info}&quot;, buffer, 32, IntPtr.Zero);

            return buffer.TrimEnd(&apos;\0&apos;);
        }



        public string DevicePath { get =&amp;gt; longpath; }
        public string DeviceShortPath { get =&amp;gt; shortName; }
        public string AliasName { get =&amp;gt; aliasName; }
        public bool SetDevicePath(string longpath)
        {
            if (aliasName != null)
                return false;

            this.longpath = longpath;
            return true;
        }
        public void Open()
        {
            if (!TryGetShortPath(longpath, out shortName))
                throw new Exception(&quot;Get short path faield when initializing.&quot;);

            aliasName = $&quot;nmci{DateTime.Now.Ticks}&quot;;
            MciSendStringWithCheck($&quot;open {shortName} alias {aliasName}&quot;, null, 0, IntPtr.Zero);
        }
        public void Close()
        {
            MciSendStringWithCheck($&quot;close {aliasName}&quot;, null, 0, IntPtr.Zero);

            aliasName = null;
        }
        public void Play()
        {
            MciSendStringWithCheck($&quot;play {aliasName}&quot;, null, 0, IntPtr.Zero);
        }
        public void Resume()
        {
            MciSendStringWithCheck($&quot;resume {aliasName}&quot;, null, 0, IntPtr.Zero);
        }
        public void Pause()
        {
            MciSendStringWithCheck($&quot;pause {aliasName}&quot;, null, 0, IntPtr.Zero);
        }
        public void Stop()
        {
            MciSendStringWithCheck($&quot;stop {aliasName}&quot;, null, 0, IntPtr.Zero);
        }
        public int GetPosition()
        {
            return int.Parse(StatusInfo(&quot;position&quot;));
        }
        public int GetLength()
        {
            return int.Parse(StatusInfo(&quot;length&quot;));
        }
        public PlaybackState GetState()
        {
            switch (StatusInfo(&quot;mode&quot;).ToLower())
            {
                case &quot;playing&quot;:
                    return PlaybackState.Playing;
                case &quot;paused&quot;:
                    return PlaybackState.Paused;
                case &quot;stopped&quot;:
                    return PlaybackState.Stopped;
                default:
                    return PlaybackState.Invalid;
            }
        }
        public void PlayWait()
        {
            MciSendStringWithCheck($&quot;play {aliasName} wait&quot;, null, 0, IntPtr.Zero);
        }
        public void PlayRepeat()
        {
            MciSendStringWithCheck($&quot;play {aliasName} repeat&quot;, null, 0, IntPtr.Zero);
        }
        public void Seek(int position)
        {
            MciSendStringWithCheck($&quot;seek {aliasName} to {position}&quot;, null, 0, IntPtr.Zero);
        }
        public void SeekToStart()
        {
            MciSendStringWithCheck($&quot;seek {aliasName} to start&quot;, null, 0, IntPtr.Zero);
        }
        public void SetSeekMode(bool fExact)
        {
            MciSendStringWithCheck($&quot;set {aliasName} seek exactly {(fExact ? &quot;on&quot; : &quot;off&quot;)}&quot;, null, 0, IntPtr.Zero);
        }
        public void Dispose()
        {
            if (aliasName != null)
                Close();
        }

    }
    public enum PlaybackState
    {
        Stopped,
        Playing,
        Paused,

        Invalid = -1,
    }
    public class MciException : Exception
    {
        private MciError err;

        public int ErrorId { get =&amp;gt; (int)err; }
        public string ErrorName { get =&amp;gt; err.ToString(); }
        public override string Message { get =&amp;gt; ErrorName; }
        public MciException() { }
        public MciException(int errorId)
        {
            if (Enum.IsDefined(typeof(MciError), errorId))
                err = (MciError)errorId;
            else
                throw new ArgumentOutOfRangeException(&quot;不是正确的错误ID&quot;);
        }
        public MciException(string errorName)
        {
            if (!Enum.TryParse&amp;lt;MciError&amp;gt;(errorName, out err))
                throw new ArgumentOutOfRangeException(&quot;不是正确的错误ID&quot;);
        }
        enum MciError
        {
            MCIERR_NO_ERROR = 0,

            MCIERR_INVALID_DEVICE_ID = 257,
            MCIERR_UNRECOGNIZED_KEYWORD = 259,
            MCIERR_UNRECOGNIZED_COMMAND = 261,
            MCIERR_HARDWARE = 262,
            MCIERR_INVALID_DEVICE_NAME = 263,
            MCIERR_OUT_OF_MEMORY = 264,
            MCIERR_DEVICE_OPEN = 265,
            MCIERR_CANNOT_LOAD_DRIVER = 266,
            MCIERR_MISSING_COMMAND_STRING = 267,
            MCIERR_PARAM_OVERFLOW = 268,
            MCIERR_MISSING_STRING_ARGUMENT = 269,
            MCIERR_BAD_INTEGER = 270,
            MCIERR_PARSER_INTERNAL = 271,
            MCIERR_DRIVER_INTERNAL = 272,
            MCIERR_MISSING_PARAMETER = 273,
            MCIERR_UNSUPPORTED_FUNCTION = 274,
            MCIERR_FILE_NOT_FOUND = 275,
            MCIERR_DEVICE_NOT_READY = 276,
            MCIERR_INTERNAL = 277,
            MCIERR_DRIVER = 278,
            MCIERR_CANNOT_USE_ALL = 279,
            MCIERR_MULTIPLE = 280,
            MCIERR_EXTENSION_NOT_FOUND = 281,
            MCIERR_OUTOFRANGE = 282,
            MCIERR_FLAGS_NOT_COMPATIBLE = 284,
            MCIERR_FILE_NOT_SAVED = 286,
            MCIERR_DEVICE_TYPE_REQUIRED = 287,
            MCIERR_DEVICE_LOCKED = 288,
            MCIERR_DUPLICATE_ALIAS = 289,
            MCIERR_BAD_CONSTANT = 290,
            MCIERR_MUST_USE_SHAREABLE = 291,
            MCIERR_MISSING_DEVICE_NAME = 292,
            MCIERR_BAD_TIME_FORMAT = 293,
            MCIERR_NO_CLOSING_QUOTE = 294,
            MCIERR_DUPLICATE_FLAGS = 295,
            MCIERR_INVALID_FILE = 296,
            MCIERR_NULL_PARAMETER_BLOCK = 297,
            MCIERR_UNNAMED_RESOURCE = 298,
            MCIERR_NEW_REQUIRES_ALIAS = 299,
            MCIERR_NOTIFY_ON_AUTO_OPEN = 300,
            MCIERR_NO_ELEMENT_ALLOWED = 301,
            MCIERR_NONAPPLICABLE_FUNCTION = 302,
            MCIERR_ILLEGAL_FOR_AUTO_OPEN = 303,
            MCIERR_FILENAME_REQUIRED = 304,
            MCIERR_EXTRA_CHARACTERS = 305,
            MCIERR_DEVICE_NOT_INSTALLED = 306,
            MCIERR_GET_CD = 307,
            MCIERR_SET_CD = 308,
            MCIERR_SET_DRIVE = 309,
            MCIERR_DEVICE_LENGTH = 310,
            MCIERR_DEVICE_ORD_LENGTH = 311,
            MCIERR_NO_INTEGER = 312,

            MCIERR_WAVE_OUTPUTSINUSE = 320,
            MCIERR_WAVE_SETOUTPUTINUSE = 321,
            MCIERR_WAVE_INPUTSINUSE = 322,
            MCIERR_WAVE_SETINPUTINUSE = 323,
            MCIERR_WAVE_OUTPUTUNSPECIFIED = 324,
            MCIERR_WAVE_INPUTUNSPECIFIED = 325,
            MCIERR_WAVE_OUTPUTSUNSUITABLE = 326,
            MCIERR_WAVE_SETOUTPUTUNSUITABLE = 327,
            MCIERR_WAVE_INPUTSUNSUITABLE = 328,
            MCIERR_WAVE_SETINPUTUNSUITABLE = 329,

            MCIERR_SEQ_DIV_INCOMPATIBLE = 336,
            MCIERR_SEQ_PORT_INUSE = 337,
            MCIERR_SEQ_PORT_NONEXISTENT = 338,
            MCIERR_SEQ_PORT_MAPNODEVICE = 339,
            MCIERR_SEQ_PORT_MISCERROR = 340,
            MCIERR_SEQ_TIMER = 341,
            MCIERR_SEQ_PORTUNSPECIFIED = 342,
            MCIERR_SEQ_NOMIDIPRESENT = 343,

            MCIERR_NO_WINDOW = 346,
            MCIERR_CREATEWINDOW = 347,
            MCIERR_FILE_READ = 348,
            MCIERR_FILE_WRITE = 349,
            MCIERR_NO_IDENTITY = 350,
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;
如果有什么建议或者这个类有可改进的地方, 欢迎留言哦~  n(*≧▽≦*)n&lt;/p&gt;
</content:encoded></item><item><title>[C#] MCI 详解与封装类, MCI 播放音乐, 获取播放状态, 获取音频长度, 进度调整,</title><link>https://slimenull.com/posts/20210211102111/</link><guid isPermaLink="true">https://slimenull.com/posts/20210211102111/</guid><description>淦!琢磨了一晚上啊, 总算有些眉目了.首先, MCI的全称是Multimedia Control Interface, 即多媒体控制接口, 通过它, 我们可以做到播放音频视频, 甚至录制音频, 虽然古老, 但是真的强大.注意, 文章较官方文档有不少删减, 如果看标准内容, 还是看官方文档比较好.MCI Command Strings 官方文档: Microsoft Command Strings - Win32 app | Microsoft Docs哦对了, 文档是纯英文哦~理论基础:</description><pubDate>Thu, 11 Feb 2021 10:21:11 GMT</pubDate><content:encoded>&lt;h2&gt;淦!&lt;/h2&gt;
&lt;p&gt;琢磨了一晚上啊, 总算有些眉目了. &amp;lt;sub&amp;gt;(所以我还是一个笨蛋啊, 明明这么简单的东西却花费了这么长时间&amp;lt;/sub&amp;gt;&lt;/p&gt;
&lt;p&gt;首先, MCI的全称是Multimedia Control Interface, 即多媒体控制接口, 通过它, 我们可以做到播放音频视频, 甚至录制音频, 虽然古老, 但是真的强大.&lt;/p&gt;
&lt;p&gt;注意, 文章较官方文档有不少删减, 如果看标准内容, 还是看官方文档比较好.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;MCI Command Strings 官方文档: &lt;a href=&quot;https://docs.microsoft.com/en-us/windows/win32/multimedia/mci-command-strings&quot;&gt;Microsoft Command Strings - Win32 app | Microsoft Docs&lt;/a&gt;
哦对了, 文档是纯英文哦~&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;理论基础:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;MCI 不能与 C# 中的内存流打交道, 他只能播放文件.&lt;/li&gt;
&lt;li&gt;MCI 支持很多格式, 包含: CD音频, 数字音频, MIDI音乐, 视频唱片(videodisc), VCR, 以及波形音频.&lt;/li&gt;
&lt;li&gt;MCI 中, 被播放的音频文件被称作 &lt;strong&gt;设备(Device)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;MCI 中, 支持巨多设置项, 包括播放进度, 音量大小, 声道开关, 如果你播放的是 MIDI, 支持的设置更多.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;正式开始:&lt;/h2&gt;
&lt;p&gt;下面, 我们将以播放音频为例, 尽可能多的讲述 MCI 的相关知识, 文章在今后可能会继续更新.&lt;/p&gt;
&lt;p&gt;示例文件在: C:\Users\Null\Desktop\Tutorial.wav&lt;/p&gt;
&lt;h4&gt;&amp;gt; 引用库:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;最最开始, 无非是载入库.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;对于 C++, 需要引用 Windows.h 以及 Mmsystem.h 这两个头文件&lt;/li&gt;
&lt;li&gt;对于 C#, using System.Runtime.InteropServices; 以进行非托管库的调用.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&amp;gt; 执行指令:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;MCI 中, 有 3 种方式来执行 MCI 指令. 分别是: mciSendString, mciExecute, mciSendCommand, 它们均位于 winmm.dll 中.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;函数原型:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MCIERROR mciSendString(    // MCIERROR 是 无符号长整形数字
    LPCTSTR lpszCommand,
    LPTSTR  lpszReturnString,
    UINT    cchReturn,
    HANDLE  hwndCallback
);
  
BOOL mciExecute(
    LPCSTR pszCommand
);

  MCIERROR mciSendCommand(
      MCIDEVICEID IDDevice,    // 设备 ID, 通过另一个函数打开文件可以获得
      UINT        uMsg,
      DWORD_PTR   fdwCommand,
      DWORD_PTR   dwParam
  );
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;C# 声明方式:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[DllImport(&quot;winmm.dll&quot;, EntryPoint = &quot;mciSendString&quot;, CharSet = CharSet.Unicode)]
extern static ulong MciSendString(string command, string buffer, int bufferSize, IntPtr callback);

[DllImport(&quot;winmm.dll&quot;, EntryPoint = &quot;mciExecute&quot;, CharSet = CharSet.Unicode)]
extern static bool MciExecute(string command);

[DllImport(&quot;winmm.dll&quot;, EntryPoint = &quot;mciSendCommand&quot;, CharSet = CharSet.Unicode)]
extern static ulong MciSendCommand(uint deviceId, uint uMsg, IntPtr fdwCommand, IntPtr dwParam);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;简略说明:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;mciSendString: 最常用的方法, 通过字符串来表示一个指令, 会返回错误码, 不会有弹窗警告
mciExecute: 调试时较常用的方法, 在执行时若有未完善的地方, 会弹窗警告, 也因为如此, 程序的发布版本不会使用这个(影响使用体验)
mciSendCommand: 很少用, 通过指令的ID来表示指令, 会返回错误码, 不会有弹窗警告
 
某些 MCI 指令具有返回值, 例如获取播放状态, 这些指令不能通过mciExecute执行.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;文章只会通过 mciSendString 来介绍 MCI 哟&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&amp;gt; 打开文件:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;对于播放音频, 首要的第一件事, 肯定就是打开文件并将其载入到内存了. 不过有一点很重要, 就是 MCI 指令只支持短路径(ShortPath), 所以在拿到文件路径后, 我们得将其转换为短路径.&lt;/li&gt;
&lt;li&gt;如果不对路径进行转换, 那么某些名字长度大于8的文件(准确来说是路径中任何一部分长度大于8)的将无法播放
&lt;blockquote&gt;
&lt;p&gt;关于短路径与长路径: &lt;a href=&quot;https://blog.csdn.net/zfs2008zfs/article/details/51154873&quot;&gt;windows系统下的文件长名和文件短名&lt;/a&gt;
短路径与长路径的转换: &lt;a href=&quot;/posts/20210209080535/&quot;&gt;[C#/C/C++] GetShortPathName 详解, 长路径转换为短路径&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;MCI 指令中, 通过 open 来打开一个文件, 并且在末尾还可以使用 &quot;alias 别名&quot; 来为这个已打开的文件起一个别名.&lt;/li&gt;
&lt;li&gt;下面是两个示例:&lt;pre&gt;&lt;code&gt;// 文件是 C:\Users\Null\Desktop\Tutorial.wav
// 转换为短路径是 C:\Users\Null\Desktop\Tut~1.wav
mciSendString(@&quot;open C:\Users\Null\Desktop\Tut~1.wav&quot;, null, 0, IntPtr.Zero);
mciSendString(@&quot;open C:\Users\Null\Desktop\Tut~1.wav alias tutorial&quot;, null, 0, IntPtr.Zero);
&lt;/code&gt;&lt;/pre&gt;
在第一的 mciSendString 中, 很清晰明了, 打开了一个文件, 而第二个中, 我们还加了一个alias, 即, 别名, MCI支持为打开的文件起一个别名, 并且推荐这么做.
第二个种, 我们为它起的别名是 tutorial.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&amp;gt; 播放音频:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;播放音频的 MCI 指令是 play, 直接 &quot;play 设备&quot;&lt;/li&gt;
&lt;li&gt;示例:&lt;pre&gt;&lt;code&gt;mciSendString(@&quot;play C:\Users\Null\Desktop\Tut~1.wav&quot;, null, 0, IntPtr.Zero);
mciSendString(@&quot;play tutorial&quot;, null, 0, IntPtr.Zero);
&lt;/code&gt;&lt;/pre&gt;
如果你没有为文件指定别名, 那么在使用 play 指令时, 只能指定短路径
如果你为文件指定了别名, 直接play加上别名即可播放这个文件.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&amp;gt; 重复播放音频:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;这里, 用到了参数, 就像alias一样, 可选. 重复播放还是使用的play指令.&lt;/li&gt;
&lt;li&gt;示例:&lt;pre&gt;&lt;code&gt;mciSendString(@&quot;play C:\Users\Null\Desktop\Tut~1.wav repeat&quot;, null, 0, IntPtr.Zero);
mciSendString(@&quot;play tutorial repeat&quot;, null, 0, IntPtr.Zero);
&lt;/code&gt;&lt;/pre&gt;
设备名如旧, 可以直接指定短路径, 也可以指定别名, 而想做到重复播放, 只需要在最后指定repeat&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&amp;gt; 同步播放音频:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;同样是参数, 这里是wait, 支持wait参数的指令可以同步执行, 例如play指令&lt;/li&gt;
&lt;li&gt;示例:&lt;pre&gt;&lt;code&gt;mciSendString(@&quot;play C:\Users\Null\Desktop\Tut~1.wav wait&quot;, null, 0, IntPtr.Zero);
mciSendString(@&quot;play tutorial wait&quot;, null, 0, IntPtr.Zero);
&lt;/code&gt;&lt;/pre&gt;
执行后, 将会阻塞当前现成, 直至播放结束.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&amp;gt; 暂停播放:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;暂停, pause, 用法很简单, 同样是 &quot;pause 设备&quot;&lt;/li&gt;
&lt;li&gt;示例:&lt;pre&gt;&lt;code&gt;mciSendString(@&quot;pause C:\Users\Null\Desktop\Tut~1.wav&quot;, null, 0, IntPtr.Zero);
mciSendString(@&quot;pause tutorial&quot;, null, 0, IntPtr.Zero);
&lt;/code&gt;&lt;/pre&gt;
执行后, 正在播放的音频就会暂停.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&amp;gt; 恢复播放:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;恢复, resume, 语法是 &quot;resume 设备&quot;&lt;/li&gt;
&lt;li&gt;示例:&lt;pre&gt;&lt;code&gt;mciSendString(@&quot;pause C:\Users\Null\Desktop\Tut~1.wav&quot;, null, 0, IntPtr.Zero);
mciSendString(@&quot;pause tutorial&quot;, null, 0, IntPtr.Zero);
&lt;/code&gt;&lt;/pre&gt;
执行后, 暂停播放的音频就会恢复.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&amp;gt; 关闭文件:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;关闭, close, 语法是: &quot;close 设备&quot;&lt;/li&gt;
&lt;li&gt;示例:&lt;pre&gt;&lt;code&gt;mciSendString(@&quot;close C:\Users\Null\Desktop\Tut~1.wav&quot;, null, 0, IntPtr.Zero);
mciSendString(@&quot;close tutorial&quot;, null, 0, IntPtr.Zero);
&lt;/code&gt;&lt;/pre&gt;
执行后, 文件会被关闭. 如果音频正在播放, 则会停止.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&amp;gt; 改变播放位置:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;seek 指令, 这个指令比较复杂哦. 语法如下:
&lt;blockquote&gt;
&lt;p&gt;seek &lt;em&gt;device&lt;/em&gt; to &lt;em&gt;position&lt;/em&gt;
seek &lt;em&gt;device&lt;/em&gt; to start
seek &lt;em&gt;device&lt;/em&gt; to end&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;示例:&lt;pre&gt;&lt;code&gt;mciSendString(@&quot;seek tutorial to 1000&quot;, null, 0, IntPtr.Zero);
mciSendString(@&quot;seek tutorial to start&quot;, null, 0, IntPtr.Zero);
mciSendString(@&quot;seek tutorial to end&quot;, null, 0, IntPtr.Zero);
&lt;/code&gt;&lt;/pre&gt;
短路径肯定是能用的哦, 我只是懒得写了, 至于 &quot;seek &lt;em&gt;device&lt;/em&gt; to &lt;em&gt;position&lt;/em&gt;&quot; 的用法,  其中position默认是ms为单位的整数时间哦, 也就是说, 1000代表1s.&lt;/li&gt;
&lt;li&gt;seek 的单位是可以调整的, 继续看哦&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&amp;gt; 设置相关:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设置, set, 这个支持的就更多了&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;以下是适用于 CD Audio 的语法 :&lt;/strong&gt;
set &lt;em&gt;device&lt;/em&gt; time format milliseconds
set &lt;em&gt;device&lt;/em&gt; time format msf
set &lt;em&gt;device&lt;/em&gt; time format tmsf&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;以下是适用于 Wave Audio 的语法 :&lt;/strong&gt;
set &lt;em&gt;device&lt;/em&gt; any input
set &lt;em&gt;device&lt;/em&gt; any output
set &lt;em&gt;device&lt;/em&gt; channels &lt;em&gt;channel_count&lt;/em&gt;
set &lt;em&gt;device&lt;/em&gt; format tag pcm
set &lt;em&gt;device&lt;/em&gt; format tag tag
set &lt;em&gt;device&lt;/em&gt; input &lt;em&gt;integer&lt;/em&gt;
set &lt;em&gt;device&lt;/em&gt; output &lt;em&gt;integer&lt;/em&gt;
set &lt;em&gt;device&lt;/em&gt; time format bytes
set &lt;em&gt;device&lt;/em&gt; time format milliseconds&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选项&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;time format milliseconds&lt;/td&gt;
&lt;td&gt;将时间格式设置为毫秒, 所有使用position值的指令都将采用毫秒作为单位, 你可以将milliseconds缩写为ms. 对于音序器设备,&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;time format msf&lt;/td&gt;
&lt;td&gt;设置时间格式到 分钟, 妙, 以及帧. 所有使用position值的指令都见采用MSF格式(CD音频的默认格式), 请将MSF值指定为 mm:ss:ff 的格式, mm是分钟, ss是秒, ff是帧. 如果一个字段以及后面的字段都是0, 你可以忽略掉它. 例如, 3, 3:0, 3:0:0 都是表示3分钟的正确方式. MSF字段有以下最大值, 分钟:99, 秒:59, 帧: 74.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;time format tmsf&lt;/td&gt;
&lt;td&gt;将时间格式设置为 音轨, 分钟, 秒, 以及帧. 所有使用position值的指令都见采用TMSF格式, 额, 与上面的一样, 只不过多了个音轨. 音轨的最大值是99, 分钟, 秒, 帧的最大值与MSF格式一致.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;any input&lt;/td&gt;
&lt;td&gt;当录制时, 使用所有支持当前格式的输入, 这是默认设置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;any output&lt;/td&gt;
&lt;td&gt;当播放时, 使用所有支持当前格式的输出, 这是默认的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;time format bytes&lt;/td&gt;
&lt;td&gt;在 PCM 文件格式中, 设置时间格式(单位)为字节, 指定这个指令后, 所有position信息都将被指定为字节格式&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&amp;gt; 状态信息:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;状态, status, 语法: &quot;status &lt;em&gt;device&lt;/em&gt; &lt;em&gt;option&lt;/em&gt;&quot;, 返回到 mciSendString 参数指定的字符串缓冲区&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;适用于音频的常用语法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;status &lt;em&gt;device&lt;/em&gt; position&lt;/p&gt;
&lt;p&gt;status &lt;em&gt;device&lt;/em&gt; length&lt;/p&gt;
&lt;p&gt;status &lt;em&gt;device&lt;/em&gt; mode&lt;/p&gt;
&lt;p&gt;status &lt;em&gt;device&lt;/em&gt; time format&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选项&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;position&lt;/td&gt;
&lt;td&gt;获取目前播放的位置单位与时间格式统一&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;length&lt;/td&gt;
&lt;td&gt;获取当前播放音频的长度 单位与时间格式统一&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mode&lt;/td&gt;
&lt;td&gt;获取播放状态, 返回的值是以下值之一: stopped / playing / paused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;time format&lt;/td&gt;
&lt;td&gt;获取当前的时间格式&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;string buffer = new string(&apos;\0&apos;, 256);       // 分配一个长度的字符串用来存放返回值
mciSendString(&quot;status tutorial position&quot;, buffer, 256, IntPtr.Zero);   // 调用
Console.WriteLine(buffer.TrimEnd(&apos;\0&apos;));     // 打印返回值, TrimEnd的原因字符串的是长度是256, 函数没有使用的部分仍然是 \0 字符.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&amp;gt; 音频设置&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设置音频, setaudio, 语法 &quot;setaudio &lt;em&gt;device&lt;/em&gt; &lt;em&gt;option&lt;/em&gt;&quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;常用语法:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;setaudio &lt;em&gt;device&lt;/em&gt; left volume to &lt;em&gt;factor&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;setaudio &lt;em&gt;device&lt;/em&gt; right volume to &lt;em&gt;factor&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;setaudio &lt;em&gt;device&lt;/em&gt; volume to &lt;em&gt;factor&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选项&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;left/right volume to &lt;em&gt;factor&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;将指定声道的音量设置为指定值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;volume to &lt;em&gt;factor&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;将音量设置为指定值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;错误码:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;下面是sendMciString会返回的错误码以及描述(对名称翻译, 然后稍加校正), 哦对了, 返回 0 代表无错误哦&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;错误码&lt;/th&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;257&lt;/td&gt;
&lt;td&gt;MCIERR_INVALID_DEVICE_ID&lt;/td&gt;
&lt;td&gt;无效设备 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;259&lt;/td&gt;
&lt;td&gt;MCIERR_UNRECOGNIZED_KEYWORD&lt;/td&gt;
&lt;td&gt;未识别关键字&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;261&lt;/td&gt;
&lt;td&gt;MCIERR_UNRECOGNIZED_COMMAND&lt;/td&gt;
&lt;td&gt;未识别的命令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;262&lt;/td&gt;
&lt;td&gt;MCIERR_HARDWARE&lt;/td&gt;
&lt;td&gt;硬件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;263&lt;/td&gt;
&lt;td&gt;MCIERR_INVALID_DEVICE_NAME&lt;/td&gt;
&lt;td&gt;无效的设备名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;264&lt;/td&gt;
&lt;td&gt;MCIERR_OUT_OF_MEMORY&lt;/td&gt;
&lt;td&gt;内存不足&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;265&lt;/td&gt;
&lt;td&gt;MCIERR_DEVICE_OPEN&lt;/td&gt;
&lt;td&gt;设备打开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;266&lt;/td&gt;
&lt;td&gt;MCIERR_CANNOT_LOAD_DRIVER&lt;/td&gt;
&lt;td&gt;无法加载驱动程序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;267&lt;/td&gt;
&lt;td&gt;MCIERR_MISSING_COMMAND_STRING&lt;/td&gt;
&lt;td&gt;缺少命令字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;268&lt;/td&gt;
&lt;td&gt;MCIERR_PARAM_OVERFLOW&lt;/td&gt;
&lt;td&gt;参数溢出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;269&lt;/td&gt;
&lt;td&gt;MCIERR_MISSING_STRING_ARGUMENT&lt;/td&gt;
&lt;td&gt;缺少字符串参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;270&lt;/td&gt;
&lt;td&gt;MCIERR_BAD_INTEGER&lt;/td&gt;
&lt;td&gt;坏整数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;271&lt;/td&gt;
&lt;td&gt;MCIERR_PARSER_INTERNAL&lt;/td&gt;
&lt;td&gt;分析器内部 (估计是这个API内部对指令分析时出现的错误)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;272&lt;/td&gt;
&lt;td&gt;MCIERR_DRIVER_INTERNAL&lt;/td&gt;
&lt;td&gt;驱动程序内部&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;273&lt;/td&gt;
&lt;td&gt;MCIERR_MISSING_PARAMETER&lt;/td&gt;
&lt;td&gt;缺少参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;274&lt;/td&gt;
&lt;td&gt;MCIERR_UNSUPPORTED_FUNCTION&lt;/td&gt;
&lt;td&gt;不支持的功能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;275&lt;/td&gt;
&lt;td&gt;MCIERR_FILE_NOT_FOUND&lt;/td&gt;
&lt;td&gt;未找到文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;276&lt;/td&gt;
&lt;td&gt;MCIERR_DEVICE_NOT_READY&lt;/td&gt;
&lt;td&gt;设备未就绪&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;277&lt;/td&gt;
&lt;td&gt;MCIERR_INTERNAL&lt;/td&gt;
&lt;td&gt;内部&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;278&lt;/td&gt;
&lt;td&gt;MCIERR_DRIVER&lt;/td&gt;
&lt;td&gt;驱动器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;279&lt;/td&gt;
&lt;td&gt;MCIERR_CANNOT_USE_ALL&lt;/td&gt;
&lt;td&gt;不能全部使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;280&lt;/td&gt;
&lt;td&gt;MCIERR_MULTIPLE&lt;/td&gt;
&lt;td&gt;多个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;281&lt;/td&gt;
&lt;td&gt;MCIERR_EXTENSION_NOT_FOUND&lt;/td&gt;
&lt;td&gt;未找到扩展&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;282&lt;/td&gt;
&lt;td&gt;MCIERR_OUTOFRANGE&lt;/td&gt;
&lt;td&gt;超出范围&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;284&lt;/td&gt;
&lt;td&gt;MCIERR_FLAGS_NOT_COMPATIBLE&lt;/td&gt;
&lt;td&gt;标志不兼容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;286&lt;/td&gt;
&lt;td&gt;MCIERR_FILE_NOT_SAVED&lt;/td&gt;
&lt;td&gt;文件未保存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;287&lt;/td&gt;
&lt;td&gt;MCIERR_DEVICE_TYPE_REQUIRED&lt;/td&gt;
&lt;td&gt;需要设备类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;288&lt;/td&gt;
&lt;td&gt;MCIERR_DEVICE_LOCKED&lt;/td&gt;
&lt;td&gt;设备已锁定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;289&lt;/td&gt;
&lt;td&gt;MCIERR_DUPLICATE_ALIAS&lt;/td&gt;
&lt;td&gt;重复别名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;290&lt;/td&gt;
&lt;td&gt;MCIERR_BAD_CONSTANT&lt;/td&gt;
&lt;td&gt;坏常量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;291&lt;/td&gt;
&lt;td&gt;MCIERR_MUST_USE_SHAREABLE&lt;/td&gt;
&lt;td&gt;必须使用可共享&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;292&lt;/td&gt;
&lt;td&gt;MCIERR_MISSING_DEVICE_NAME&lt;/td&gt;
&lt;td&gt;缺少设备名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;293&lt;/td&gt;
&lt;td&gt;MCIERR_BAD_TIME_FORMAT&lt;/td&gt;
&lt;td&gt;错误的时间格式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;294&lt;/td&gt;
&lt;td&gt;MCIERR_NO_CLOSING_QUOTE&lt;/td&gt;
&lt;td&gt;没有关闭中的引用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;295&lt;/td&gt;
&lt;td&gt;MCIERR_DUPLICATE_FLAGS&lt;/td&gt;
&lt;td&gt;重复标志&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;296&lt;/td&gt;
&lt;td&gt;MCIERR_INVALID_FILE&lt;/td&gt;
&lt;td&gt;无效文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;297&lt;/td&gt;
&lt;td&gt;MCIERR_NULL_PARAMETER_BLOCK&lt;/td&gt;
&lt;td&gt;空参数块&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;298&lt;/td&gt;
&lt;td&gt;MCIERR_UNNAMED_RESOURCE&lt;/td&gt;
&lt;td&gt;未命名的资源&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;299&lt;/td&gt;
&lt;td&gt;MCIERR_NEW_REQUIRES_ALIAS&lt;/td&gt;
&lt;td&gt;新需要别名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;MCIERR_NOTIFY_ON_AUTO_OPEN&lt;/td&gt;
&lt;td&gt;自动打开时通知&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;301&lt;/td&gt;
&lt;td&gt;MCIERR_NO_ELEMENT_ALLOWED&lt;/td&gt;
&lt;td&gt;不允许任何元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;302&lt;/td&gt;
&lt;td&gt;MCIERR_NONAPPLICABLE_FUNCTION&lt;/td&gt;
&lt;td&gt;不可应用功能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;303&lt;/td&gt;
&lt;td&gt;MCIERR_ILLEGAL_FOR_AUTO_OPEN&lt;/td&gt;
&lt;td&gt;非法自动打开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;304&lt;/td&gt;
&lt;td&gt;MCIERR_FILENAME_REQUIRED&lt;/td&gt;
&lt;td&gt;需要文件名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;305&lt;/td&gt;
&lt;td&gt;MCIERR_EXTRA_CHARACTERS&lt;/td&gt;
&lt;td&gt;额外字符 (可能是指多出了一些不需要的字符)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;306&lt;/td&gt;
&lt;td&gt;MCIERR_DEVICE_NOT_INSTALLED&lt;/td&gt;
&lt;td&gt;未安装设备&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;307&lt;/td&gt;
&lt;td&gt;MCIERR_GET_CD&lt;/td&gt;
&lt;td&gt;获取 CD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;308&lt;/td&gt;
&lt;td&gt;MCIERR_SET_CD&lt;/td&gt;
&lt;td&gt;设置 CD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;309&lt;/td&gt;
&lt;td&gt;MCIERR_SET_DRIVE&lt;/td&gt;
&lt;td&gt;设置驱动器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;310&lt;/td&gt;
&lt;td&gt;MCIERR_DEVICE_LENGTH&lt;/td&gt;
&lt;td&gt;设备长度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;311&lt;/td&gt;
&lt;td&gt;MCIERR_DEVICE_ORD_LENGTH&lt;/td&gt;
&lt;td&gt;设备 ORD 长度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;312&lt;/td&gt;
&lt;td&gt;MCIERR_NO_INTEGER&lt;/td&gt;
&lt;td&gt;无整数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;320&lt;/td&gt;
&lt;td&gt;MCIERR_WAVE_OUTPUTSINUSE&lt;/td&gt;
&lt;td&gt;波输出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;321&lt;/td&gt;
&lt;td&gt;MCIERR_WAVE_SETOUTPUTINUSE&lt;/td&gt;
&lt;td&gt;波设置输出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;322&lt;/td&gt;
&lt;td&gt;MCIERR_WAVE_INPUTSINUSE&lt;/td&gt;
&lt;td&gt;波输入使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;323&lt;/td&gt;
&lt;td&gt;MCIERR_WAVE_SETINPUTINUSE&lt;/td&gt;
&lt;td&gt;波设置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;324&lt;/td&gt;
&lt;td&gt;MCIERR_WAVE_OUTPUTUNSPECIFIED&lt;/td&gt;
&lt;td&gt;波输出未指定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;325&lt;/td&gt;
&lt;td&gt;MCIERR_WAVE_INPUTUNSPECIFIED&lt;/td&gt;
&lt;td&gt;波输入未指定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;326&lt;/td&gt;
&lt;td&gt;MCIERR_WAVE_OUTPUTSUNSUITABLE&lt;/td&gt;
&lt;td&gt;波输出可居住&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;327&lt;/td&gt;
&lt;td&gt;MCIERR_WAVE_SETOUTPUTUNSUITABLE&lt;/td&gt;
&lt;td&gt;波设置通普通西装&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;328&lt;/td&gt;
&lt;td&gt;MCIERR_WAVE_INPUTSUNSUITABLE&lt;/td&gt;
&lt;td&gt;波输入可居住&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;329&lt;/td&gt;
&lt;td&gt;MCIERR_WAVE_SETINPUTUNSUITABLE&lt;/td&gt;
&lt;td&gt;波设置通图适合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;336&lt;/td&gt;
&lt;td&gt;MCIERR_SEQ_DIV_INCOMPATIBLE&lt;/td&gt;
&lt;td&gt;Seq Div 不兼容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;337&lt;/td&gt;
&lt;td&gt;MCIERR_SEQ_PORT_INUSE&lt;/td&gt;
&lt;td&gt;SEQ 端口 INUSE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;338&lt;/td&gt;
&lt;td&gt;MCIERR_SEQ_PORT_NONEXISTENT&lt;/td&gt;
&lt;td&gt;Seq 端口不存在&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;339&lt;/td&gt;
&lt;td&gt;MCIERR_SEQ_PORT_MAPNODEVICE&lt;/td&gt;
&lt;td&gt;Seq 端口地图无设备&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;340&lt;/td&gt;
&lt;td&gt;MCIERR_SEQ_PORT_MISCERROR&lt;/td&gt;
&lt;td&gt;SEQ 杂项错误&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;341&lt;/td&gt;
&lt;td&gt;MCIERR_SEQ_TIMER&lt;/td&gt;
&lt;td&gt;SEQ 定时器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;342&lt;/td&gt;
&lt;td&gt;MCIERR_SEQ_PORTUNSPECIFIED&lt;/td&gt;
&lt;td&gt;SEQ 端口未指定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;343&lt;/td&gt;
&lt;td&gt;MCIERR_SEQ_NOMIDIPRESENT&lt;/td&gt;
&lt;td&gt;SEQ 没有MIDI在场&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;346&lt;/td&gt;
&lt;td&gt;MCIERR_NO_WINDOW&lt;/td&gt;
&lt;td&gt;无窗口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;347&lt;/td&gt;
&lt;td&gt;MCIERR_CREATEWINDOW&lt;/td&gt;
&lt;td&gt;创建窗口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;348&lt;/td&gt;
&lt;td&gt;MCIERR_FILE_READ&lt;/td&gt;
&lt;td&gt;文件读取&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;349&lt;/td&gt;
&lt;td&gt;MCIERR_FILE_WRITE&lt;/td&gt;
&lt;td&gt;文件写入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;350&lt;/td&gt;
&lt;td&gt;MCIERR_NO_IDENTITY&lt;/td&gt;
&lt;td&gt;无标识&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;封装类:&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;去这里吧, 在我的另一篇文章中 &lt;a href=&quot;/posts/20210211104239/&quot;&gt;[C#] 音乐播放 3 种方式 Demo 与 MCI 音乐播放器封装类.&lt;/a&gt; o(〃&apos;▽&apos;〃)o&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;附加内容:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;下面是我的一些发现
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;WinForm 程序使用 MCI 是可以打开 MP3 文件的, 但是如果是控制台程序, 就会出现错误, 错误码266,  &quot;MCIERR_CANNOT_LOAD_DRIVER&quot;&lt;/li&gt;
&lt;li&gt;MCI 的某些指令不能正常使用, 但其实并不是很影响, 例如, &quot;set &lt;em&gt;device&lt;/em&gt; audio &lt;em&gt;left/right&lt;/em&gt; &lt;em&gt;off/off&lt;/em&gt;&quot;, 无法正常使用.&lt;/li&gt;
&lt;li&gt;音量控制我目前还是没弄成... 不过可以确认的是, 进度获取, 调整, 长度获取是没问题的, 有这些最基本的, 就差不多公用了呢&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;放一张我写文章时的照片吧 , 原文贴的哪都是 (笑&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210209110959116.png&quot; alt=&quot;手动翻译, 淦&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>[C#/C/C++] GetShortPathName详解, 长路径转换为短路径</title><link>https://slimenull.com/posts/20210209080535/</link><guid isPermaLink="true">https://slimenull.com/posts/20210209080535/</guid><description>说点骚话:转换需要用到 Windows API (废话)官方文档: GetShortPathNameW function (fineapi.h) - Win32 apps | Microsoft docs  (纯英文, 没有中文版本.)引用命名空间:using System.Runtime.InteropServices;关键代码:C#[DllImport(&quot;kernel32.dll&quot;, EntryPoint = &quot;GetShortPathNameW&quot;, CharSet = CharSe</description><pubDate>Tue, 09 Feb 2021 08:05:35 GMT</pubDate><content:encoded>&lt;h2&gt;说点骚话:&lt;/h2&gt;
&lt;p&gt;转换需要用到 Windows API (废话)&lt;/p&gt;
&lt;p&gt;官方文档: &lt;a href=&quot;https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getshortpathnamew&quot;&gt;GetShortPathNameW function (fineapi.h) - Win32 apps | Microsoft docs&lt;/a&gt;  (纯英文, 没有中文版本.)&lt;/p&gt;
&lt;h2&gt;引用命名空间:&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;using System.Runtime.InteropServices;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;关键代码:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;csharp&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;[DllImport(&quot;kernel32.dll&quot;, EntryPoint = &quot;GetShortPathNameW&quot;, CharSet = CharSet.Unicode)]
extern static short GetShortPath(string longPath, string buffer, int bufferSize);

// 获取转换所需要的缓冲区大小
bool TryGetConvertBufferSize(string longPath, out short size)
{
    size = GetShortPath(longPath, null, 0);
    return size != 0;
}

// 进行转换
bool TryGetShortPath(string longPath, out string shortPath, int bufferSize = 256)
{
    string resultBuffer = new string(&apos;\0&apos;, bufferSize);               // 256 大概合适, 根据需求调整吧
    short rstLen = GetShortPath(longPath, resultBuffer, bufferSize);
    if (rstLen != 0 &amp;amp;&amp;amp; rstLen != bufferSize)
    {
        shortPath = resultBuffer.TrimEnd(&apos;\0&apos;);
        return true;
    }
    else
    {
        shortPath = null;
        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;C++&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    long     length = 0;
    TCHAR*   buffer = NULL;

// 初次通过传递NULL和0来获取所需空间

    length = GetShortPathName(lpszPath, NULL, 0);
    if (length == 0) ErrorExit(TEXT(&quot;GetShortPathName&quot;));

// 动态分配准确的内存

    buffer = new TCHAR[length];

// 现在, 使用同一个长路径来简单调用函数吧

    length = GetShortPathName(lpszPath, buffer, length);
    if (length == 0) ErrorExit(TEXT(&quot;GetShortPathName&quot;));

    _tprintf(TEXT(&quot;long name = %s shortname = %s&quot;), lpszPath, buffer);
    
    delete [] buffer;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;更多说明:&lt;/h2&gt;
&lt;h4&gt;内容 1:&lt;/h4&gt;
&lt;p&gt;如果设置了 buffer, 与 bufferSize, 函数会尝试转换长路径到短路径, 如果返回的数字与 bufferSize 一致, 则表示缓冲区(buffer)太小.&lt;/p&gt;
&lt;p&gt;如果 buffer 被设为 null, bufferSize 被设为 0, 则返回值将返回所需要的缓冲区大小.&lt;/p&gt;
&lt;p&gt;无论如何, 如果返回值是 0, 则表示函数由于某些原因, 未能成功执行.&lt;/p&gt;
&lt;p&gt;获取更多的错误信息, 请调用 &lt;a href=&quot;https://docs.microsoft.com/en-us/windows/desktop/api/errhandlingapi/nf-errhandlingapi-getlasterror&quot;&gt;CallLastError&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原文:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If the function succeeds, the return value is the length, in TCHARs, of the string that is copied to lpszShortPath, not including the terminating null character.
 
If the lpszShortPath buffer is too small to contain the path, the return value is the size of the buffer, in TCHARs, that is required to hold the path and the terminating null character.
 
If the function fails for any other reason, the return value is zero. To get extended error information, call &lt;a href=&quot;https://docs.microsoft.com/en-us/windows/desktop/api/errhandlingapi/nf-errhandlingapi-getlasterror&quot;&gt;GetLastError&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;内容 2:&lt;/h4&gt;
&lt;p&gt;在这个函数(GetShortPathName)的 ANSI 版本中, 路径长度被限制在 MAX_PATH(260) 个字符. 要拓展这个限制到 23767 宽字符(wide character), 请调用这个函数的 Unicode 版本, 并且在路径的首部加上 &quot;\\?\&quot;. 更多信息, 请查阅 &lt;a href=&quot;https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file&quot;&gt;文件, 路径, 以及命名空间的命名&lt;/a&gt;
&lt;strong&gt;原文:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In the ANSI version of this function, the name is limited to MAX_PATH characters. To extend this limit to 32,767 wide characters, call the Unicode version of the function and prepend &quot;\?&quot; to the path. For more information, see Naming Files, Paths, and Namespaces.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;内容 3:&lt;/h4&gt;
&lt;p&gt;关于 Windows API 函数的 Unicode 版本和 ANSI 版本的区别, 可参阅: &lt;a href=&quot;/posts/20201120033237/&quot;&gt;Windows API 函数后缀的作用&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>[C#] WinForm 与 WPF 获取命令行参数</title><link>https://slimenull.com/posts/20210209030419/</link><guid isPermaLink="true">https://slimenull.com/posts/20210209030419/</guid><description>推荐方法:using System;Environment.GetCommandLineArgs();   // 返回 string[]注意, 与控制台程序入口处的string[] args相比较, 这个函数返回的结果是完整的命令行, 也就是包含程序自身路径. 例如我一个没有传递任何参数的程序:所以注意区分哦.其他:WinForm在 Program.cs 的 Main 入口参数处添加 string[] args, 然后你可以更改窗体的构造函数, 使其能够接收这个args.WPF暂时不</description><pubDate>Tue, 09 Feb 2021 03:04:19 GMT</pubDate><content:encoded>&lt;h3&gt;推荐方法:&lt;/h3&gt;
&lt;h4&gt;1. Environment.GetCommandLineArgs();&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;using System;
Environment.GetCommandLineArgs();   // 返回 string[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意, 与控制台程序入口处的string[] args相比较, 这个函数返回的结果是完整的命令行, 也就是包含程序自身路径. 例如我一个没有传递任何参数的程序:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210209030243165.png&quot; alt=&quot;&quot; /&gt;
所以注意区分哦.&lt;/p&gt;
&lt;h4&gt;&lt;/h4&gt;
&lt;h3&gt;其他:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WinForm
在 Program.cs 的 Main 入口参数处添加 string[] args, 然后你可以更改窗体的构造函数, 使其能够接收这个args.&lt;/li&gt;
&lt;li&gt;WPF
暂时不知道.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[踩坑记录] C# 源代码被误删, 通过反编译之前编译好的文件来拯救这一切, 三款反编译工具对比</title><link>https://slimenull.com/posts/20210206012402/</link><guid isPermaLink="true">https://slimenull.com/posts/20210206012402/</guid><description>事件:首先, 语言是C#, 然后我把代码给删了… 但是在删之前, 我编译过一次, 所以… 我就想着使用反编译工具来搞了.我知道的有两个反编译工具, dnSply 和 ILSpydnSpy是ILSpy的一个分支(dnSpy is a .NET assembly editor, decompiler, and debugger forked from ILSpy)开始吧:&gt; 首先, 是 dnSpy 上场:乍一看, 没啥问题, 但是, 会有很多毛病哦~ 一一道来吧:dnSpy 的反编译结果</description><pubDate>Sat, 06 Feb 2021 01:24:02 GMT</pubDate><content:encoded>&lt;h2&gt;事件:&lt;/h2&gt;
&lt;p&gt;首先, 语言是C#, 然后我把代码给删了... 但是在删之前, 我编译过一次, 所以... 我就想着使用反编译工具来搞了.&lt;/p&gt;
&lt;p&gt;我知道的反编译工具, dnSply, ILSpy, dotPeek&lt;/p&gt;
&lt;p&gt;dnSpy是ILSpy的一个分支(dnSpy is a .NET assembly editor, decompiler, and debugger forked from ILSpy)&lt;/p&gt;
&lt;p&gt;dotPeek 是 JetBrains 的反编译工具, 在这之前我还没使用过.&lt;/p&gt;
&lt;h2&gt;开始吧:&lt;/h2&gt;
&lt;h5&gt;&amp;gt; 首先, 是 dnSpy 上场:&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210206003510436.png&quot; alt=&quot;&quot; /&gt;
乍一看, 没啥问题, 但是, 会有很多毛病哦~ 一一道来吧:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;dnSpy 的反编译结果, 似乎不会吧字段和方法放在一起, 我也没找到它反编译的字段定义跑哪去了...&lt;/li&gt;
&lt;li&gt;会有一大堆奇怪代码, 跟我们想要的源代码完全不一致, 即便导出为项目, 也会有无数个错误&lt;/li&gt;
&lt;li&gt;总之, dnSpy是个反编译的好工具, 但还不能够完全获取源代码...&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总之先导出一下吧, 选中你的项目, 然后程序左上角, 文件(File) -&amp;gt; 导出为项目(Export to project)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210206011738335.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后就是... 惨不忍睹... 左下角的错误数量 99+ 了已经, 完全没有纠错的必要.
&lt;img src=&quot;/images/20210206011653138.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;&amp;gt; 然后, 就是 ILSpy 上场了:&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210206010849149.png&quot; alt=&quot;&quot; /&gt;
嗯好吧, 我承认 ILSpy 没有 dnSpy 好看, 但这可不是决定实力的第一要素.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;你看到了, 枚举, 字段, 方法, 一切都井然有序&lt;/li&gt;
&lt;li&gt;仔细看看, 中间的那个 switch 语句, 瞧瞧! 这不是 C#8 的新语法嘛! (我当初没有使用这个语法哦)&lt;/li&gt;
&lt;li&gt;完全没有任何奇怪的变量名&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那么我们尝试导出项目吧! 在左侧选中项目, 右击, 选择 Save Code 以导出项目&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210206011243537.png&quot; alt=&quot;&quot; /&gt;
感动世界!!! 没有任何错误! 非常非常非常完美~
&lt;img src=&quot;/images/20210206011344812.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;&amp;gt; 再试试 dotPeak:&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;/images/2021020602061066.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;哦豁, 看样子, 也不错啊.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;dotPeek 没有像 ILSpy 那样将 switch 还原为 C#8 的语法, 而是使用的旧的语法&lt;/li&gt;
&lt;li&gt;没有奇怪的变量名, 奇怪的语法&lt;/li&gt;
&lt;li&gt;出现了我没有写过的东西, 标签&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;导出项目咯, 直接右击项目, 然后 Export to Project&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/2021020602081143.png&quot; alt=&quot;&quot; /&gt;
唔, 一开始就与众不同呢... 跟普通的项目完全不一样, 少了一些文件夹...&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210206021027247.png&quot; alt=&quot;&quot; /&gt;
嘛, 总之先试着打开吧.
果然在VS里面一眼就能看出错误了!
&lt;img src=&quot;/images/20210206021524681.png&quot; alt=&quot;&quot; /&gt;
它居然在本地方法的前头中加了static... 好吧好吧, 我知道它们本质上是静态的, 但你这么弄, 我可就不能编译了呀. (假设我很懒, 懒到连这仅有的一个错误也不愿意改)&lt;/p&gt;
&lt;h2&gt;总结:&lt;/h2&gt;
&lt;p&gt;如果说我平常使用, 我肯定会用 dnSpy (颜值高), 毕竟平常还是很少改源代码甚至导出为项目滴, 如果是像这次的特殊情况, 我就会用 ILSpy, 很强大的有木有? 至于 dotPeek, 打入冷宫了, 而且加载速度慢的一批,,, 等它启动完, 黄花菜都凉了.&lt;/p&gt;
&lt;p&gt;也算是大成功了, 所以,,, 如果想要通过反编译来挽回删掉的文件, 那么请使用 ILSpy 吧! 经过这一次使用, 我也是爱上 ILSpy了, 毕竟一开始我没报有这么大希望.&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;
如果你觉得有什么改进的, 或者有什么下期文章的建议, 评论区见ovo&lt;/p&gt;
</content:encoded></item><item><title>[踩坑记录] 某功能在C#7.3中不可用,请使用 8.0 或更高的语言版本 (通用解决方案)</title><link>https://slimenull.com/posts/20210206002632/</link><guid isPermaLink="true">https://slimenull.com/posts/20210206002632/</guid><description>问题:正如标题描述, 是某个语法在低版本中不受支持, 只需要升级到高版本即可.步骤:&gt; 第一种 :使用快捷键 Alt + Enter 或点击黄色的的提示调出快速操作选择 将该项目升级为 C# 语言版本 “8.0”如果没有这一选项, 继续看第二种.&gt; 第二种 :打开项目所在目录并打开项目文件.在 PropertyGroup 节点下添加 LangVersion, 如图:第二种其实就是通过更改 csproj 文件内容来置顶所使用的语言版本, 某些时候可能在这个</description><pubDate>Sat, 06 Feb 2021 00:26:32 GMT</pubDate><content:encoded>&lt;h2&gt;问题:&lt;/h2&gt;
&lt;p&gt;正如标题描述, 是某个语法在低版本中不受支持, 只需要升级到高版本即可.&lt;/p&gt;
&lt;h2&gt;步骤:&lt;/h2&gt;
&lt;h4&gt;&lt;strong&gt;&amp;gt; 第一种 :&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;使用快捷键 Alt + Enter 或点击黄色的的提示调出快速操作&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/20200917143210762.png&quot; alt=&quot;&quot; /&gt;
2. 选择 将该项目升级为 C# 语言版本 &quot;8.0&quot;
&amp;gt; 如果没有这一选项, 继续看第二种.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;&amp;gt; 第二种 :&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;打开项目所在目录并打开项目文件.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210206001925422.png&quot; alt=&quot;&quot; /&gt;
2. 在 PropertyGroup 节点下添加 LangVersion, 如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210206002242856.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;第二种其实就是通过更改 csproj 文件内容来置顶所使用的语言版本, 某些时候可能在这个文件中没有 LangVerison 节点, 就导致了快速操作中没有 &quot;升级项目版本&quot; 的选项, 这时就需要手动改文件了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;&lt;strong&gt;&amp;gt; 第三种 :&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;在 Visual Studio 中右击你的项目, 选择 &apos;卸载项目&apos;&lt;/li&gt;
&lt;li&gt;右击项目, 点击 &apos;编辑项目文件&apos;&lt;/li&gt;
&lt;li&gt;然后按照第二种方式, 直接在 PropertyGroup 下面添加 LangVersion 为 latest&lt;/li&gt;
&lt;li&gt;编辑完之后, 右击项目, 点击 &apos;重新加载项目&apos;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;温馨提示&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Visual Studio 的项目默认存储目录是: %UserProfile%\source\repos, 直接在文件资源管理器的路径栏输入这个就可以跳转, 如果你不知道 %UserProfile% 是什么意思, 你可以在网上搜索 &apos;Windows 环境变量&apos;, 或者如果想看我写的文章, 在评论区评论就彳亍咯&lt;/li&gt;
&lt;li&gt;在 Visual Studio 中, 右击项目, 可以打开项目所在文件夹, &apos;在 Explorer 中打开目录&apos; 或者 &apos;在文件资源管理器中打开目录&apos;, 所以不要再问我找不到该怎么办啦&lt;/li&gt;
&lt;li&gt;有的项目中可能你并不能找到 PropertyGroup, 没关系, 自己添加一个也彳亍, 反正 Visual Studio 都能正常读取的, 但你得保证 PropertyGroup 是在最外层的噢&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;
如果还是没能解决你的问题, 麻烦在下面评论下, 这样我可以搜寻更多答案以修改这边文章.
如果解决了, 就点个赞吧 (卑微ovo)&lt;/p&gt;
</content:encoded></item><item><title>[干货] 杜绝广告, 让你的电脑变的更加干净舒适</title><link>https://slimenull.com/posts/20210204060513/</link><guid isPermaLink="true">https://slimenull.com/posts/20210204060513/</guid><description>从根部治理, 拒绝流氓软件:广告弹窗啊, 无非是用了流氓软件. 所以说, 少用流氓软件, 至于哪些是流氓软件, 杀毒软件我从来没用过(只用Windows Defender). 至于其它的, 能少用国内的就少用(忠告), 毕竟某些厂商的尿性, 咱都懂.然后, 就是捆绑, 这个很简单, 其实原因就是你使用了所谓的&quot;高速下载器&quot;, 而那些&quot;高速下载器&quot;在你电脑上安装了一堆你不需要的软件, 而避免方式就是, 永远不使用高速下载, 只选择本地下载或者地名+运营商的下载按钮. 例如, 上海电信下载.还有就是,</description><pubDate>Thu, 04 Feb 2021 06:05:13 GMT</pubDate><content:encoded>&lt;h3&gt;从根部治理, 拒绝流氓软件:&lt;/h3&gt;
&lt;p&gt;广告弹窗啊, 无非是用了流氓软件. 所以说, 少用流氓软件, 至于哪些是流氓软件, 杀毒软件我从来没用过(只用Windows Defender). 至于其它的, 能少用国内的就少用(忠告), 毕竟某些厂商的尿性, 咱都懂.
&lt;img src=&quot;/images/20210204033616149.png&quot; alt=&quot;流氓软件&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后, 就是捆绑, 这个很简单, 其实原因就是你使用了所谓的&quot;高速下载器&quot;, 而那些&quot;高速下载器&quot;在你电脑上安装了一堆你不需要的软件, 而避免方式就是, 永远不使用高速下载, 只选择本地下载或者地名+运营商的下载按钮. 例如, 上海电信下载.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210204033644914.png&quot; alt=&quot;捆绑安装&quot; /&gt;
还有就是, XXX下载站, XXX软件园, 这些地方少去, 下载软件还是去官方网站比较好.&lt;/p&gt;
&lt;p&gt;然后就是流氓软件了, 国内最大的流氓, 莫过于杀毒软件了, 所以, 把你的360, 电脑管家, 金山毒霸, 鲁大师全部卸载了吧. 如果是火绒倒是可以留着, 这不算是流氓, 不过意义也不大就是了.&lt;/p&gt;
&lt;p&gt;然后就是其它的流氓, WinRAR, Flash player, WPS之类的, WinRAR和Flash Player建议直接卸载, WPS看可以通过控制面板的&quot;卸载或更改&quot;来关掉广告, 这个随便一搜索就能找到.&lt;/p&gt;
&lt;h3&gt;替代品:&lt;/h3&gt;
&lt;p&gt;既然说了不使用流氓软件, 肯定就还有推荐的软件的.&lt;/p&gt;
&lt;p&gt;杀毒软件, 一般是不需要的, 除非你天天浏览奇怪的网站... 但即便那样, Windows Defender 也够用.&lt;/p&gt;
&lt;p&gt;输入法. 不要用搜狗输入法! 不要用搜狗输入法! 不要用搜狗输入法! 这东西广告多的一批... 我跟你讲噢, windows是有自带的中文输入法的, 连日文以及其他语言也有... 所以完全没必要安装多余的输入法.
在 设置 -&amp;gt; 时间和语言 -&amp;gt; 语言 中添加中文, 然后点击中文, 点击选项, 在下面的键盘那里, 添加一个键盘, 微软拼音, 如果已经有了就不需要添加. 语言和键盘添加完毕后, 就可以通过 Win + 空格 键来切换语言, 哦对了, 如果你有一个 英文语言, 会好很多噢.
Ctrl + Shift 是切换同一个语言内的不同键盘(输入法), Win + 空格 是直接切换语言, 这个要知道.&lt;/p&gt;
&lt;p&gt;压缩包管理软件, 不使用WinRAR, 也不使用好压什么的软件, 你可以使用7zip, Bandzip, 或者360压缩的国际版.&lt;/p&gt;
&lt;p&gt;浏览器, 不要使用360的任何一款浏览器,,, 因为它本身就有内置广告... 还有很多浏览器打的什么双核的幌子, 其实双核就是Chromium核+IE核, 现在IE核心已经没啥乱用了. 所以, 老老实实用系统自带的Edge, 如果你是Win7, 就去搜索Edge, 然后去官网下载. 或者, 你也可以用 Chrome, Firefox, 不过据我所知, 新Edge跟Chrome是一个核心的, 并且Edge优化了内存占用, 更推荐Edge.&lt;/p&gt;
&lt;p&gt;然后是即时通讯软件, 不推荐使用QQ, 因为这玩意儿也有弹窗广告, 并且是关不掉的, 推荐使用TIM, 不用担心, TIM是QQ的办公版本, 与QQ的数据是同步的, 而且更为简洁方便, 所以, 非常推荐哦, 搜索TIM就能找到官网了&lt;/p&gt;
&lt;p&gt;然后就是,,, 娱乐? 腾讯视频, 爱奇艺, 优酷, 这些软件我是不用的, 并且也不推荐使用. 我看视频消遣一向是哔哩哔哩(网页, 且无任何视频广告, 没错, 没有视频广告!)&lt;/p&gt;
&lt;p&gt;音乐的话, 我是用的网易云音乐, 没发现什么流氓行为.&lt;/p&gt;
&lt;p&gt;然后是办公软件, 如果你是使用的WPS, 推荐你更换为Office, 只要你电脑是整机购买的, 大多都是带有Office的, Office比WPS流畅很多呢. 如果你用习惯了WPS, 不愿更换, 并且忍受不了它的广告弹窗, 通过控制面板, 然后点卸载WPS(其实是&quot;更改或卸载&quot;), 里面是可以配置广告弹窗的&lt;/p&gt;
&lt;h3&gt;如果遇到广告弹窗?&lt;/h3&gt;
&lt;p&gt;这里呢, 再安利一个软件, 叫做Process Explorer, 里面有个功能, 可以通过窗口来查找进程, 操作非常的简单, 你呢, 在网上稍微搜索一下使用教程, 就彳亍, 通过它, 你可以很轻易的找到这个广告的主程序. 最后, 看看你是否认识他, 如果认识,,, 就想办法禁用掉广告, 网上会有一些方法的, 我也提两个吧.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先把后缀改为txt, 然后你就可以作为文本编辑它了, 打开它, 清空所有内容, 保存, 然后改为原来的后缀.&lt;/li&gt;
&lt;li&gt;找到 uninstall.exe 类似的程序, 直接卸载完事儿.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;保养措施:&lt;/h3&gt;
&lt;p&gt;在安装了一个软件后, 千万千万要检查它是否开机自动启动, 保证开机启动的都是你想要启动的程序.&lt;/p&gt;
&lt;h3&gt;最后:&lt;/h3&gt;
&lt;p&gt;我个人已经很久没见过弹窗广告了 (笑, 大概就是上面的那些经验了, 精髓就是, 绝对不使用流氓软件, 尽量避免使用国内软件.&lt;/p&gt;
</content:encoded></item><item><title>[项目实例] 手把手教你使用Python写一个功能非常强大的翻译程序</title><link>https://slimenull.com/posts/20210204050241/</link><guid isPermaLink="true">https://slimenull.com/posts/20210204050241/</guid><description>关于:程序是借助API的(废话), 所以在无网络时, 没办法使用. 成品下载地址在文章末.准备工作:导入库, request(发送请求) 和 json(解析返回数据).开搞:首先, 是找到我们所需要的API, 瞧瞧这个链接: 有道翻译体验虽说是体验, 但是我都用了巨长一段时间却没有任何问题, 看来这个API至少短时间内不会有任何变更了~然后就是破解API的请求内容了. 打开浏览器调试, 转到网络选项卡, 然后重新翻译一下, 好的, 结果很明显, 芜湖 ????post请求, 3个参数,</description><pubDate>Thu, 04 Feb 2021 05:02:41 GMT</pubDate><content:encoded>&lt;h2&gt;关于:&lt;/h2&gt;
&lt;p&gt;程序是借助API的(废话), 所以在无网络时, 没办法使用. 成品下载地址在文章末.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210204040516540.png&quot; alt=&quot;成果&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;准备工作:&lt;/h3&gt;
&lt;p&gt;导入库, request(发送请求) 和 json(解析返回数据).&lt;/p&gt;
&lt;h3&gt;开搞:&lt;/h3&gt;
&lt;p&gt;首先, 是找到我们所需要的API, 瞧瞧这个链接: &lt;a href=&quot;https://ai.youdao.com/product-fanyi-text.s&quot;&gt;有道翻译体验&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210204040735158.png&quot; alt=&quot;有道翻译体验&quot; /&gt;
虽说是体验, 但是我都用了巨长一段时间却没有任何问题, 看来这个API至少短时间内不会有任何变更了~&lt;/p&gt;
&lt;p&gt;然后就是破解API的请求内容了. 打开浏览器调试, 转到网络选项卡, 然后重新翻译一下, 好的, 结果很明显, 芜湖 :-D
&lt;img src=&quot;/images/20210204041108490.png&quot; alt=&quot;破解API&quot; /&gt;
post请求, 3个参数, q是翻译内容, from和to表示翻译的语言, 那么, 试着换一下语言吧.
&lt;img src=&quot;/images/20210204041301694.png&quot; alt=&quot;中文翻译为日文&quot; /&gt;
哦, 我们得到了中文与日文的对应数据, zh-CHS 和 ja, 还有更多语言呢, 看看HTML节点吧
&lt;img src=&quot;/images/20210204041441767.png&quot; alt=&quot;看看英文&quot; /&gt;
&lt;img src=&quot;/images/20210204041544186.png&quot; alt=&quot;结果&quot; /&gt;
哦豁, 比想象中的要简单! 2就是to的意思, 那么, 我们能得到所有的语言对应参数值了!
&lt;img src=&quot;/images/20210204041758373.png&quot; alt=&quot;HTML节点很清晰啊&quot; /&gt;
记录下来吧~&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 参数: q:翻译文本   from:从语言   to:至语言
 语言支持的值: 中文:zh-CHS, 英语:en, 日语:ja, 韩语:ko, 法语:fr, 俄语:ru, 
     西班牙语:es, 葡萄牙语:pt, 越南语:vi, 德语:de, 印尼语:id
     阿拉伯语:ar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来, 就是对网页中, 源代码的进一步分析了, 检查&quot;翻译&quot;按钮的节点, 然后查看click事件监听代码.
&lt;img src=&quot;/images/20210204042129770.png&quot; alt=&quot;检查按钮&quot; /&gt;
&lt;img src=&quot;/images/20210204042414558.png&quot; alt=&quot;代码找到&quot; /&gt;
&lt;img src=&quot;/images/20210204042519905.png&quot; alt=&quot;源代码&quot; /&gt;
&lt;img src=&quot;/images/20210204042621283.png&quot; alt=&quot;返回值描述到手&quot; /&gt;
然后, 回到刚刚的网络选项卡, 然后重新翻译一下, 查看返回值.
&lt;img src=&quot;/images/20210204042857110.png&quot; alt=&quot;返回值&quot; /&gt;
弄到 Visual Studio Code里面, 保存为json文件, 然后格式化文档.
&lt;img src=&quot;/images/20210204043152952.png&quot; alt=&quot;格式化文档&quot; /&gt;
&lt;img src=&quot;/images/20210204043225607.png&quot; alt=&quot;翻译结果到手&quot; /&gt;
但是, 不止如此, 我们再尝试翻译一些, 词, 例如 sugar, 然后看看返回值, 不出所料, 果然!
&lt;img src=&quot;/images/20210204043446223.png&quot; alt=&quot;暗藏玄机&quot; /&gt;
返回值里面包含的内容不是一般的多啊, 甚至还有音标, hhh&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;returnPhrase&quot;: [
        &quot;sugar&quot;
    ],
    &quot;query&quot;: &quot;sugar\n&quot;,
    &quot;errorCode&quot;: &quot;0&quot;,
    &quot;l&quot;: &quot;en2zh-CHS&quot;,
    &quot;tSpeakUrl&quot;: &quot;http://openapi.youdao.com/ttsapi?q=%E7%B3%96&amp;amp;langType=zh-CHS&amp;amp;sign=B212B7714049BBB65257DC9D64234EA0&amp;amp;salt=1612384414308&amp;amp;voice=4&amp;amp;format=mp3&amp;amp;appKey=2423360539ba5632&amp;amp;ttsVoiceStrict=false&quot;,
    &quot;web&quot;: [
        {
            &quot;value&quot;: [
                &quot;糖&quot;,
                &quot;食糖&quot;,
                &quot;白糖&quot;,
                &quot;糖分&quot;
            ],
            &quot;key&quot;: &quot;Sugar&quot;
        },
        {
            &quot;value&quot;: [
                &quot;红糖&quot;,
                &quot;黄糖&quot;
            ],
            &quot;key&quot;: &quot;brown sugar&quot;
        },
        {
            &quot;value&quot;: [
                &quot;糖用甜菜&quot;,
                &quot;甜菜&quot;,
                &quot;糖甜菜&quot;,
                &quot;糖料甜菜&quot;
            ],
            &quot;key&quot;: &quot;Sugar beet&quot;
        }
    ],
    &quot;requestId&quot;: &quot;f54b0c24-4818-4d7e-ab04-8123adec8671&quot;,
    &quot;translation&quot;: [
        &quot;糖&quot;
    ],
    &quot;dict&quot;: {
        &quot;url&quot;: &quot;yddict://m.youdao.com/dict?le=eng&amp;amp;q=sugar%0A&quot;
    },
    &quot;webdict&quot;: {
        &quot;url&quot;: &quot;http://m.youdao.com/dict?le=eng&amp;amp;q=sugar%0A&quot;
    },
    &quot;basic&quot;: {
        &quot;exam_type&quot;: [
            &quot;初中&quot;,
            &quot;高中&quot;,
            &quot;CET4&quot;,
            &quot;CET6&quot;,
            &quot;考研&quot;,
            &quot;SAT&quot;
        ],
        &quot;us-phonetic&quot;: &quot;ˈʃʊɡər&quot;,
        &quot;phonetic&quot;: &quot;ˈʃʊɡə(r)&quot;,
        &quot;uk-phonetic&quot;: &quot;ˈʃʊɡə(r)&quot;,
        &quot;wfs&quot;: [
            {
                &quot;wf&quot;: {
                    &quot;name&quot;: &quot;复数&quot;,
                    &quot;value&quot;: &quot;sugars&quot;
                }
            },
            {
                &quot;wf&quot;: {
                    &quot;name&quot;: &quot;过去式&quot;,
                    &quot;value&quot;: &quot;sugared&quot;
                }
            },
            {
                &quot;wf&quot;: {
                    &quot;name&quot;: &quot;过去分词&quot;,
                    &quot;value&quot;: &quot;sugared&quot;
                }
            },
            {
                &quot;wf&quot;: {
                    &quot;name&quot;: &quot;现在分词&quot;,
                    &quot;value&quot;: &quot;sugaring&quot;
                }
            },
            {
                &quot;wf&quot;: {
                    &quot;name&quot;: &quot;第三人称单数&quot;,
                    &quot;value&quot;: &quot;sugars&quot;
                }
            }
        ],
        &quot;uk-speech&quot;: &quot;http://openapi.youdao.com/ttsapi?q=sugar%0A&amp;amp;langType=en&amp;amp;sign=C668ACDF9C58F3D76BADC5B54F1BDEE7&amp;amp;salt=1612384414308&amp;amp;voice=5&amp;amp;format=mp3&amp;amp;appKey=2423360539ba5632&amp;amp;ttsVoiceStrict=false&quot;,
        &quot;explains&quot;: [
            &quot;n. 糖；食糖；甜言蜜语&quot;,
            &quot;vt. 加糖于；粉饰&quot;,
            &quot;vi. 形成糖&quot;,
            &quot;n. (Sugar)人名；(英)休格&quot;
        ],
        &quot;us-speech&quot;: &quot;http://openapi.youdao.com/ttsapi?q=sugar%0A&amp;amp;langType=en&amp;amp;sign=C668ACDF9C58F3D76BADC5B54F1BDEE7&amp;amp;salt=1612384414308&amp;amp;voice=6&amp;amp;format=mp3&amp;amp;appKey=2423360539ba5632&amp;amp;ttsVoiceStrict=false&quot;
    },
    &quot;isWord&quot;: true,
    &quot;speakUrl&quot;: &quot;http://openapi.youdao.com/ttsapi?q=sugar%0A&amp;amp;langType=en&amp;amp;sign=C668ACDF9C58F3D76BADC5B54F1BDEE7&amp;amp;salt=1612384414308&amp;amp;voice=4&amp;amp;format=mp3&amp;amp;appKey=2423360539ba5632&amp;amp;ttsVoiceStrict=false&quot;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是不是已经有些感到兴奋了呢? 现在, 开始正式Coding吧!&lt;/p&gt;
&lt;p&gt;先是以自动识别语言开始吧. 那么, 首先是最基本的导库与定义:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests
import json

url = &apos;https://aidemo.youdao.com/trans&apos;
&apos;&apos;&apos;
有道翻译API, 支持get与post
参数: q:翻译文本   from:从语言   to:至语言
 语言支持的值: 中文:zh-CHS, 英语:en, 日语:ja, 韩语:ko, 法语:fr, 俄语:ru, 
     西班牙语:es, 葡萄牙语:pt, 越南语:vi, 德语:de, 印尼语:id
     阿拉伯语:ar
 错误码: 411:过于频繁   413:访问上限   103:字符太长   410:过于频繁
&apos;&apos;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后是请求&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def translate(text):
    try:
        data = {&quot;q&quot;: text, &quot;from&quot;: &quot;auto&quot;, &quot;to&quot;: &quot;auto&quot;}
        resp = requests.post(url, data)
    except:
        return None
    return resp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后是打印翻译结果:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;advancedmode = False      # 默认状态下, 不展示所有信息, 当advancedMode为True时, 展示更多内容

def listPrint(obj, iden, ps = &apos;&apos;):
    print(&quot;\n&quot;.join(&apos; &apos; * iden + ps + str(i) for i in obj))

def printrst(resp):
    if resp is not None and resp.status_code == 200:
        respJson = json.loads(resp.text)
        if advancedmode:
            print(&quot; &amp;lt;0&amp;gt; Result:&quot;)
            if &quot;translation&quot; in respJson:
                print(&quot;       Normal:&quot;)
                listPrint(respJson[&quot;translation&quot;], 9)
            if &quot;basic&quot; in respJson and &quot;explains&quot; in respJson[&quot;basic&quot;]:
                print(&quot;       Basic:&quot;)
                listPrint(respJson[&quot;basic&quot;][&quot;explains&quot;], 9)
            if &quot;web&quot; in respJson:
                print(&quot;       From web:&quot;)
                index = 1
                for i in respJson[&quot;web&quot;]:
                    print(&quot;         %d. %s:&quot; % (index, i[&quot;key&quot;]))
                    listPrint(i[&quot;value&quot;], 14)
                    index += 1
            # print
        else:
            print(&quot; &amp;lt;0&amp;gt; Result:&quot;)
            listPrint(respJson[&quot;translation&quot;], 7)
    else:
        print(&quot;Request error, status code: %s&quot; % (resp.status_code if resp != None else &quot;???&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;既然都有advancedMode了, 所以得让用户有办法打开关闭它, 模拟简单的命令行吧, 如果输入的内容是以 $ 开头的, 则判定为命令, 并运行这个命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def runcmd(cmd):
    cmdline = cmd.split(&apos; &apos;)
    cmdname = cmdline[0].lower()
    if cmdname == &apos;$&apos;:
        global advancedmode
        advancedmode = not advancedmode
        print(&quot; &amp;lt;$&amp;gt; AdvancedMode: %s&quot; % advancedmode)
    elif cmdname == &apos;$help&apos;:
        if len(cmdline) == 1:
            print(&quot; &amp;lt;$&amp;gt; Help document, type &apos;$help command&apos; for more information.&quot;)
            listPrint((
                &quot;$      : Switch Advanced Mode.&quot;,
                &quot;$help  : Display help document.&quot;
                &quot;&quot;
            ), 7)
        elif len(cmdline) == 2:
            if cmdline[1] == &apos;$&apos;:
                print(&quot; &amp;lt;$&amp;gt; Switch Advanced Mode.&quot;)
                listPrint((
                    &quot;Advanced Mode: %s&quot; % advancedmode,
                    &quot;If advanced mode is enabled, more information will be shown. default value is disabled.&quot;
                ), 7)
            else:
                print(&quot; &amp;lt;$&amp;gt; Help document: unknown command.&quot;)
        else:
            print(&quot; &amp;lt;$&amp;gt; Help document: unknown usage.&quot;)
    else:
        print(&quot; &amp;lt;$&amp;gt; Unknown command.&quot;)   # 更多指令, 你来添加吧, 例如切换语言
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后是主程序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if __name__ == &quot;__main__&quot;:
    print(&quot;Null.Translator, by Null, type &apos;$help&apos; for help. ref: Youdao AI translation api.&quot;)
    while True:
        text = input(&quot;\n&amp;gt;&amp;gt;&amp;gt; &quot;)   # 模拟像Python那样的提示符, 很酷炫的有木有
        if text != &apos;&apos;:
            if text.startswith(&apos;$&apos;):
                runcmd(text)
            else:
                resp = translate(text)
                printrst(resp)
        else:
            exit()     # 如果输入为空, 则退出, 方便退出程序. (表示懒得挪鼠标 滑稽)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(最后, 上面所有的代码片, 拼在一起, 就能运行, 懒得拼的话, 就点击下面的下载链接下载吧)&lt;/p&gt;
&lt;p&gt;再多说一句, 那个API返回的数据辣么多, 想实现什么功能自己随便实现啦~&lt;/p&gt;
&lt;h3&gt;下载链接:&lt;/h3&gt;
&lt;p&gt;CSDN站内下载: &lt;a href=&quot;https://download.csdn.net/download/m0_46555380/15031403&quot;&gt;Null.Translator&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h3&gt;预告:&lt;/h3&gt;
&lt;p&gt;这可不就是一个文本转语音API嘛! 薅有道羊毛真不戳
&lt;img src=&quot;/images/20210204050132590.png&quot; alt=&quot;哈哈&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>[C#] 单文件编译 - 编译时嵌入dll</title><link>https://slimenull.com/posts/20210203065600/</link><guid isPermaLink="true">https://slimenull.com/posts/20210203065600/</guid><description>1.打开 NuGet 包管理器位于 工具 -&gt; NuGet 包管理器 -&gt; 管理解决方案的 NuGet 程序包2. 安装搜索 Costura.Fody 并将其安装到你的项目3. 起飞然后, 进行编译, 你就会发现! 所有的dll全部被打包进exe中啦~~~...</description><pubDate>Wed, 03 Feb 2021 06:56:00 GMT</pubDate><content:encoded>&lt;h3&gt;1.打开 NuGet 包管理器&lt;/h3&gt;
&lt;p&gt;位于 工具 -&amp;gt; NuGet 包管理器 -&amp;gt; 管理解决方案的 NuGet 程序包&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210203064653427.png&quot; alt=&quot;打开NuGet包管理器&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2. 安装&lt;/h3&gt;
&lt;p&gt;搜索 Costura.Fody 并将其安装到你的项目
&lt;img src=&quot;/images/20210203065115536.png&quot; alt=&quot;安装Costura.Fody&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3. 起飞&lt;/h3&gt;
&lt;p&gt;然后, 进行编译, 你就会发现! 所有的dll全部被打包进exe中啦~~~
&lt;img src=&quot;/images/20210203065438794.png&quot; alt=&quot;但文件发布&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>[项目实例] 使用 IronPython 库来创建一个支持使用Python脚本操作的简易文本编辑器</title><link>https://slimenull.com/posts/20210203064309/</link><guid isPermaLink="true">https://slimenull.com/posts/20210203064309/</guid><description>步骤 :打开 nuget 包管理器, 工具 -&gt; NuGet 包管理器 -&gt; 管理解决方案的 NuGet 程序包.在 nuget 包管理器中找到 IronPython, 安装到你的项目.using 所需的命名空间, Microsoft.Scripting, Microsoft.Scripting.Hosting, Microsoft.Win32, IronPython.Hosting.创建 Python 引擎:ScriptEngine engine = Python</description><pubDate>Wed, 03 Feb 2021 06:43:09 GMT</pubDate><content:encoded>&lt;h2&gt;步骤 :&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;打开 nuget 包管理器, 工具 -&amp;gt; NuGet 包管理器 -&amp;gt; 管理解决方案的 NuGet 程序包.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210203030753805.png&quot; alt=&quot;nuget包管理器&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 nuget 包管理器中找到 IronPython, 安装到你的项目.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210203030547151.png&quot; alt=&quot;nuget中的IronPython&quot; /&gt;
3. using 所需的命名空间, Microsoft.Scripting, Microsoft.Scripting.Hosting, Microsoft.Win32, IronPython.Hosting.
4. 创建 Python 引擎:
&lt;code&gt;csharp     ScriptEngine engine = Python.CreateEngine();     &lt;/code&gt;
5. 创建定义域(Scope), 它用来存储变量:
&lt;code&gt;csharp     ScriptScope scope = engine.CreateScope();     &lt;/code&gt;
6. 在 Scope 中设置与获取变量值:
&lt;code&gt;csharp     scope.SetVariable(&quot;name&quot;, &quot;value&quot;);   // 其中, name是变量名, &quot;value&quot;可以是任意类型, 表示变量值     &lt;/code&gt;
获取变量也差不多, 是GetVariable.
7. 设置引擎的标准输出流以捕获内容:
&lt;code&gt;csharp     TriggerStream stream = new TriggerStream();  // TriggerStream是一个能够在写入时触发事件的, 继承了Stream的类.     engine.Runtime.IO.SetOutput(stream, Encoding.Default);  // 这样, 我们可以通过TriggerStream的写入事件来获取写入的内容     &lt;/code&gt;
提示: 了解 TriggerStream, 请查看这篇文章: &lt;a href=&quot;/posts/20210203030237/&quot;&gt;支持事件的Stream&lt;/a&gt;, 关于为什么使用 Encoding.Default 而不使用 UTF-8, 是因为在 Windows 里面, 都是用的 ANSI. 而 Default 就是获取 ANSI 的编码(在中国是GBK)
8. 执行 Python 代码:
&lt;code&gt;csharp     ScriptSource thisSrc = engine.CreateScriptSourceFromString(&quot;print(&apos;hello world&apos;)&quot;, SourceCodeKind.File)     thisSrc.Execute(scope);   // 代码在这个定义域种执行     &lt;/code&gt;
SourceCodeKind.File指这个字符串是来自文件的代码, 也就是说你可以在里面加换行加缩进以定义一个语句块之类的. 还有一个就是SourceCodeKind.Expression, 指一个表达式.&lt;/p&gt;
&lt;h2&gt;项目 :&lt;/h2&gt;
&lt;p&gt;项目已经在 GitHub 开源, 地址: &lt;a href=&quot;https://github.com/SlimeNull/Null.TextEditor&quot;&gt;SlimeNull/Null.TextEditor&lt;/a&gt;
想直接下载成品? 希望你能下载成功: &lt;a href=&quot;https://github.com/SlimeNull/Null.TextEditor/raw/main/TextEditor/bin/Release/Null.TextEditor.exe&quot;&gt;Release/Null.TextEditor.exe&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>[C#] Stream 支持写入读取触发事件的类库 继承Stream基类</title><link>https://slimenull.com/posts/20210203030237/</link><guid isPermaLink="true">https://slimenull.com/posts/20210203030237/</guid><description>[C#] Stream 支持写入读取触发事件的类库实现了 :你可以将这个流类的实例提供给某些东西, 在它操作这个流时, 你可以通过事件来接收到消息, 并加以处理, 例如拒绝写入, 或在写入前判断写入的内容. 你可以稍微改动一下这个类以适应你的需求.应用场景: 例如你使用了 IronPython 库, 并使用它执行了一些操作, 你希望 IronPython 每次 print 时, 你都能获取到内容, 则, 你可以使用这个触发流(TriggerStream)类, 将 IronPython 引擎的标准输出</description><pubDate>Wed, 03 Feb 2021 03:02:37 GMT</pubDate><content:encoded>&lt;h2&gt;[C#] Stream 支持写入读取触发事件的类库&lt;/h2&gt;
&lt;h3&gt;实现了 :&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;你可以将这个流类的实例提供给某些东西, 在它操作这个流时, 你可以通过事件来接收到消息, 并加以处理, 例如拒绝写入, 或在写入前判断写入的内容. 你可以稍微改动一下这个类以适应你的需求.&lt;/li&gt;
&lt;li&gt;应用场景: 例如你使用了 IronPython 库, 并使用它执行了一些操作, 你希望 IronPython 每次 print 时, 你都能获取到内容, 则, 你可以使用这个触发流(TriggerStream)类, 将 IronPython 引擎的标准输出流设置为触发流的实例, 这样, 每当 IronPython 有内容输出时, 你都能获取到内容. 例如这个项目: &lt;a href=&quot;https://github.com/SlimeNull/Null.TextEditor&quot;&gt;使用IronPython来制作一个支持py脚本操作内容的简易文本编辑器&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;应用场景: 例如, 你运行了一个 Process, 并且希望实时获取它的输出内容, 而不是运行完之后一次性获取所有内容, 同理, 你可以设置它的标准输出流为触发流实例, 然后每次它输出内容你都能接收到.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;源代码 :&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. 首先是仅仅包含触发器, 而不会有任何存储行为的类&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System;
using System.IO;

namespace Null.Library.TriggerStream
{
    class WriteStreamEventArgs : EventArgs
    {
        public byte[] Buffer;
        public int Offset, Count;

        public bool Denied = false;
    }
    class TriggerStream : Stream
    {
        public override bool CanRead =&amp;gt; false;

        public override bool CanSeek =&amp;gt; false;

        public override bool CanWrite =&amp;gt; true;

        public override long Length =&amp;gt; 0;

        public override long Position { get =&amp;gt; 0; set {  } }

        public override void Flush() { }

        public override int Read(byte[] buffer, int offset, int count)
        {
            return 0;
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            return 0;
        }

        public override void SetLength(long value) { }

        public override void Write(byte[] buffer, int offset, int count)
        {
            if (PreviewWrite != null)
                PreviewWrite.Invoke(this, new WriteStreamEventArgs()
                {
                    Buffer = buffer,
                    Offset = offset,
                    Count = count,
                });
        }
        public event EventHandler&amp;lt;WriteStreamEventArgs&amp;gt; PreviewWrite;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很明显能够看出, 上面的这个, 仅有事件触发, 而Seek, Read, Position等方法及属性, 都是直接使用的空或者返回合适的固定值.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 然后是带有存储功能的(使用MemoryStream)类&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System;
using System.IO;

namespace Null.Library.EventedStream
{
    class ReadStreamEventArgs : EventArgs
    {
        public byte[] Buffer;
        public int Offset, Count;

        public bool Denied = false;
    }
    class WriteStreamEventArgs : EventArgs
    {
        public byte[] Buffer;
        public int Offset, Count;

        public bool Denied = false;
    }
    public class FlushStreamEventArgs : EventArgs
    {
        public bool Denied = false;
    }
    public class SetStreamLengthEventArgs : EventArgs
    {
        public long Value;

        public bool Denied = false;
    }
    public class SeekStreamEventArgs : EventArgs
    {
        public long Offset;
        public SeekOrigin SeekOrigin;

        public bool Denied = false;
    }
    class EventedStream : Stream, IDisposable
    {
        MemoryStream baseMemory = new MemoryStream();
        public override bool CanRead =&amp;gt; baseMemory.CanRead;

        public override bool CanSeek =&amp;gt; baseMemory.CanSeek;

        public override bool CanWrite =&amp;gt; baseMemory.CanWrite;

        public override long Length =&amp;gt; baseMemory.Length;

        public override long Position { get =&amp;gt; baseMemory.Position; set =&amp;gt; baseMemory.Position = value; }

        public override void Flush()
        {
            if (PreviewFlush != null)
            {
                FlushStreamEventArgs args = new FlushStreamEventArgs();
                PreviewFlush.Invoke(this, args);
                if (args.Denied)
                    return;
            }

            baseMemory.Flush();
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            if (PreviewRead != null)
            {
                ReadStreamEventArgs args = new ReadStreamEventArgs()
                {
                    Buffer = buffer,
                    Offset = offset,
                    Count = count
                };
                PreviewRead.Invoke(this, args);
                if (args.Denied)
                    return 0;
            }

            return baseMemory.Read(buffer, offset, count);
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            if (PreviewSeek != null)
            {
                SeekStreamEventArgs args = new SeekStreamEventArgs()
                {
                    Offset = offset,
                    SeekOrigin = origin
                };
                PreviewSeek.Invoke(this, args);
                if (args.Denied)
                    return Position;
            }

            return baseMemory.Seek(offset, origin);
        }

        public override void SetLength(long value)
        {
            if (PreviewSetLength != null)
            {
                SetStreamLengthEventArgs args = new SetStreamLengthEventArgs()
                {
                    Value = value
                };
                PreviewSetLength.Invoke(this, args);
                if (args.Denied)
                    return;
            }

            baseMemory.SetLength(value);
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            if (PreviewWrite != null)
            {
                WriteStreamEventArgs args = new WriteStreamEventArgs()
                {
                    Buffer = buffer,
                    Offset = offset,
                    Count = count
                };
                PreviewWrite.Invoke(this, args);
                if (args.Denied)
                    return;
            }

            baseMemory.Write(buffer, offset, count);
        }
        public new void Dispose()
        {
            baseMemory.Dispose();
        }


        public event EventHandler&amp;lt;FlushStreamEventArgs&amp;gt; PreviewFlush;
        public event EventHandler&amp;lt;SetStreamLengthEventArgs&amp;gt; PreviewSetLength;
        public event EventHandler&amp;lt;SeekStreamEventArgs&amp;gt; PreviewSeek;
        public event EventHandler&amp;lt;WriteStreamEventArgs&amp;gt; PreviewWrite;
        public event EventHandler&amp;lt;ReadStreamEventArgs&amp;gt; PreviewRead;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;原理 :&lt;/h3&gt;
&lt;p&gt;倒也简单, 直接继承基类, 然后实现方法即可.&lt;/p&gt;
&lt;h3&gt;提示 :&lt;/h3&gt;
&lt;p&gt;如果你要同时使用这两个,,, 别忘记稍微移动下事件参数使它们在同一个文件中, 并使两个事件流类using事件参数的命名空间, 否则, 在不完全指定命名空间的状况下, 会出现不明确引用的错误.&lt;/p&gt;
</content:encoded></item><item><title>[踩坑记录] ASP.NET Core System.Data.SqlTypes.SqlNullValueException: 数据为空。不能对空值调用此方法</title><link>https://slimenull.com/posts/20210129050708/</link><guid isPermaLink="true">https://slimenull.com/posts/20210129050708/</guid><description>问题是出在这里的:SoftwareInfo result = new SoftwareInfo(reader.GetString(1),              // reader 是 MySqlDataReader 实例reader.GetString(7)){    ID = reader.GetInt32(0),    Label = reader.GetString(2),    Coder = reader.GetString(3),    DownloadCount = rea</description><pubDate>Fri, 29 Jan 2021 05:07:08 GMT</pubDate><content:encoded>&lt;h2&gt;问题是出在这里的:&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;SoftwareInfo result = new SoftwareInfo(
reader.GetString(1),              // reader 是 MySqlDataReader 实例
reader.GetString(7))
{
    ID = reader.GetInt32(0),
    Label = reader.GetString(2),
    Coder = reader.GetString(3),
    DownloadCount = reader.GetInt32(4),
    Stars = reader.GetFloat(5),
    Introduction = reader.GetString(6)
};

return result;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;解决方案:&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;第一种:&lt;/strong&gt; 添加 Null 检查, 在调用 GetString, GetInt32, GetFloat 这类包含具体的类型转换的函数前, 先调用 IsDBNull 检查这条数据是不是 Null.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SoftwareInfo currentInfo = new SoftwareInfo(
reader.GetString(1),
reader.GetString(7))
{
    ID = reader.GetInt32(0),
    Label = reader.IsDBNull(2) ? null : reader.GetString(2),
    Coder = reader.IsDBNull(3) ? null : reader.GetString(3),
    DownloadCount = reader.IsDBNull(4) ? 0 : reader.GetInt32(4),
    Stars = reader.IsDBNull(5) ? 0 : reader.IsDBNull(5) ? 0 : reader.GetFloat(5),
    Introduction = reader.IsDBNull(6) ? null : reader.GetString(6)
};

return currentInfo;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第二种:&lt;/strong&gt; 直接指定数据库的字段不可为 Null, 从根本上杜绝空值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alter table tablename modify columnname not null;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;原因:&lt;/h2&gt;
&lt;p&gt;如果数据是 Null, 就无法调用包含具体类型的方法, 因为它们内部涉及到类型转换.&lt;/p&gt;
</content:encoded></item><item><title>[Android] Android Studio入门 - 1.初识IDE</title><link>https://slimenull.com/posts/20210120045213/</link><guid isPermaLink="true">https://slimenull.com/posts/20210120045213/</guid><description>Android 入门:我也是初学 Android Studio (甚至不到一个星期), 也察觉到了… 关于这方面的图文资料居然那么少, 全都是视频教程, 为了想要节省时间的同学着想, 我挺身而出了.不是适合所有人哦~ 你得懂一些英文, 你需要有Java基础, 不需要技术多厉害, 会写Hello world就行.但是如果你不满足条件, 推荐你去B站找视频教程.下载IDE下载肯定简单咯, 直接去官网吧: 链接地址, 然后, 额, 我相信你的英文水平足以支持你找到下载按钮的,安装IDE安装也</description><pubDate>Wed, 20 Jan 2021 04:52:13 GMT</pubDate><content:encoded>&lt;h1&gt;Android 入门:&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;我也是初学 Android Studio (甚至不到一个星期), 也察觉到了... 关于这方面的图文资料居然那么少, 全都是视频教程, 为了想要节省时间的同学着想, 我挺身而出了.&lt;/li&gt;
&lt;li&gt;不是适合所有人哦~ 你得懂一些英文, 你需要有Java基础, 不需要技术多厉害, 会写Hello world就行.但是如果你不满足条件, 推荐你去B站找视频教程.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;下载IDE&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;下载肯定简单咯, 直接去官网吧: &lt;a href=&quot;https://developer.android.google.cn/studio&quot;&gt;链接地址&lt;/a&gt;, 然后, 额, 我相信你的英文水平足以支持你找到下载按钮的,&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;安装IDE&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;安装也差不多是傻瓜式安装了, 因为本人也是刚入门, 所以, 差不多是一路同意接受确认. 噢需要提到的是, Android Studio 支持使用 Java 或者 Kotlin 来编写程序, Kotlin 是编程语言哦, 这个知道就好.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;创建项目&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;打开Android后, 点击Create a project, 然后你看到的是这个:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/202101200242537.png&quot; alt=&quot;新建项目&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;看到了吗? 这是模板, 也就是给你的安卓程序创建最开始的一个页面, 不过作为我们初学者, 使用 Empty Activity 就好了~ Activity 的话, 其实就是相当于 Android 程序中的一个页面咯, 那么我们继续(Next), 你会看到项目配置:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210120024550603.png&quot; alt=&quot;配置项目&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;能看懂吧, 名字, 包名, 保存路径, &lt;strong&gt;语言&lt;/strong&gt;, 是的, 在 Language 那里需要选择你要使用的编程语言, 例如我这里使用Java. 然后, 包名的命名, 你可以这么命名: &quot;域名.你的名字.项目名&quot;, 然后 Minimum SDK, 最低版本的SDK, 这个决定了你能够使用的功能(因为不同版本的SDK功能会有差异)以及程序的兼容性(例如使用了高版本SDK的程序无法在Android版本较低的手机上运行), 其他内容的话, 我相信你能看懂.
&lt;img src=&quot;/images/20210120025320263.png&quot; alt=&quot;新项目&quot; /&gt;&lt;/li&gt;
&lt;li&gt;锵锵~ 新项目创建完成了~&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;菜单栏&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;额, 讲到这里, 用过IDEA的童鞋肯定会直接芜湖起飞的, 因为, Android Studio有JetBrains内味儿, 说不熟悉是假的, 不过,,, 既然都有Java基础了, 这点英文应该彳亍口巴? 常用的也就那么点了, 像是换配色, 更换快捷键, 你可以在网上搜索, 然后慢慢的, 你就会用了.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;项目结构&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;呐, 这是最初的项目了, 先莫得慌张, 我们得先知道左侧那一堆东西是干啥用的.
&lt;img src=&quot;/images/20210120025602149.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/li&gt;
&lt;li&gt;首先看到的就是 MainActivity, 对吧, 前面说了 Activity 是一个界面, 这个, 就是主界面了. 接下来, 我们展开这些目录, 然后看看所有的东西
&lt;img src=&quot;/images/20210120030951809.png&quot; alt=&quot;项目结构&quot; /&gt;&lt;/li&gt;
&lt;li&gt;大概就是这些内容了, 对于入门教程, 你只需要关心layout文件夹(存放布局文件)以及java源代码文件夹.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;编辑界面&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;首先就是打开我们主页面(MainActivity)的布局文件, MainActivity.xml
&lt;img src=&quot;/images/20210120031145459.png&quot; alt=&quot;打开文件&quot; /&gt;&lt;/li&gt;
&lt;li&gt;可视化编辑页面出现了
&lt;img src=&quot;/images/20210120031801701.png&quot; alt=&quot;可视化设计器&quot; /&gt;&lt;/li&gt;
&lt;li&gt;然后, 点击右上角的Split(被我用红色线框标注了的按钮), 切换到拆分设计模式, 然后, 左侧代码, 右侧是布局预览, 可以边写代码边查看效果.&lt;img src=&quot;/images/20210120032751745.png&quot; alt=&quot;布局&quot; /&gt;&lt;/li&gt;
&lt;li&gt;一般的, 我们还是使用代码来编辑界面的, 具体这些内容是什么意思, 我们下篇文章再讲 (咕咕咕?)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;界面代码&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;打开 MainActivity.java, 额, 如图所示
&lt;img src=&quot;/images/20210120033608524.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/li&gt;
&lt;li&gt;喏, 然后就是最基本的解释了. &lt;img src=&quot;/images/20210120034206180.png&quot; alt=&quot;Activity的代码&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;创建AVD&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;AVD, 即Android virtual device, 类似于虚拟机的玩意儿, 虚拟的安卓机, 可以用来运行你的程序, 所以在运行我们的程序之前, 必须得创建一个AVD, 点击AVD管理器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210120040302878.png&quot; alt=&quot;打开AVD管理器&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这就是AVD管理器了, 那么创建你的第一个虚拟设备吧~&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210120040412778.png&quot; alt=&quot;AVD管理器&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在这里选择你想要使用的设备配置, 然后继续
&lt;img src=&quot;/images/20210120041407170.png&quot; alt=&quot;创建AVD&quot; /&gt;&lt;/li&gt;
&lt;li&gt;接下来就是选择系统镜像了, 默认是没有下载的, 你需要下载一个你想要使用的系统镜像, 点击系统镜像名称后的下载按钮以下载它 &lt;img src=&quot;/images/20210120041643674.png&quot; alt=&quot;安卓系统镜像&quot; /&gt;&lt;/li&gt;
&lt;li&gt;等待他下载吧, 1GB也不是很大.
&lt;img src=&quot;/images/20210120041738934.png&quot; alt=&quot;组件安装&quot; /&gt;&lt;/li&gt;
&lt;li&gt;下载完成后, 选择你想要使用的系统镜像, 然后继续&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210120041947977.png&quot; alt=&quot;继续&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最后是验证配置信息 &lt;img src=&quot;/images/20210120042310230.png&quot; alt=&quot;验证配置&quot; /&gt;&lt;/li&gt;
&lt;li&gt;哦对了, 有一条需要注意,  展开高级配置, 然后找到 Performance, Boot option, 看到了吗? Cold boot 和 Quick boot, 这俩类似于电脑的快速启动, 如果使用 Cold boot, 每一次启动这个虚拟设备, 都跟手机开机一样, 如果使用Quick boot, 那启动这个虚拟设备, 就是从上次退出时保存的状态启动, 宛如电脑从休眠状态启动. (如果你还是不懂, 就去搜索一下什么是休眠). 之说注意这一点, 是因为, 如果你这个设备死机了, 并且你使用的是Quick boot, 那你关闭虚拟设备再打开虚拟设备, 还是死机的状态... 所及建议改为Cold boot.&lt;/li&gt;
&lt;li&gt;万事具备后, 就点Finish吧.
&lt;img src=&quot;/images/20210120043315170.png&quot; alt=&quot;创建完成&quot; /&gt;&lt;/li&gt;
&lt;li&gt;哦对了, 原生安卓玩起来真的超级舒服哦~&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;运行程序&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;点击运行按钮即可编译项目并在指定的设备上运行程序
&lt;img src=&quot;/images/20210120043723490.png&quot; alt=&quot;运行程序&quot; /&gt;&lt;/li&gt;
&lt;li&gt;需要知道的是, 运行了的虚拟设备是已经开启了调试功能的, 在你查看完运行结果后, 不必关闭虚拟设备, 虚拟设备随时为你的调试工作待命! 同时, 你也可以用你的手机进行调试, 使用数据线连接手机和电脑, 然后打开手机的调试功能, 在 Android Studio 中, 运行按钮左侧的选择设备中, 能够找到你的手机.
&lt;img src=&quot;/images/20210120044806943.png&quot; alt=&quot;运行程序&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;原生安卓&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;原生安卓, 可能有些人不大会用吧(例如笔者), 这里就说一点吧, 查看所有已安装的程序, 只需要在主屏幕从导航键或任意空白处向上滑动, 就能彳亍了
&lt;img src=&quot;/images/2021012004442282.png&quot; alt=&quot;查看所有应用程序&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结束了&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;哈, 彳亍了口巴, 通过这篇文章, 绝对能消除你对Android Studio的恐惧了, 关于更多教程, 会继续更新的, 敬请关注~&lt;/li&gt;
&lt;li&gt;有什么建议, 或者需要改正的地方, 可以通过评论或私信指出哦, 别忘了点个赞~&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[Web] 浅谈 Get 与 Post 请求</title><link>https://slimenull.com/posts/20210105173252/</link><guid isPermaLink="true">https://slimenull.com/posts/20210105173252/</guid><description>Get 与 Post 请求HTTP请求:前端与后端的数据传递是通过 HTTP 请求实现的, 浏览器(前端)向服务器(后端)发送一个 HTTP 请求, 后端对请求进行处理, 然后再给浏览器发一个响应, 这就是 HTTP 的基本概念.get 和 post 是HTTP请求的两种方式, 最明显的区别是传递参数.如果你不大了解 HTTP 请求, 可以先查看文章末对 HTTP 的简述.Get:关于 Get 请求get 一般用来获取数据, 因为其本意就是获取. 浏览器访问一个页面时, 发送的第一</description><pubDate>Tue, 05 Jan 2021 17:32:52 GMT</pubDate><content:encoded>&lt;h2&gt;Get 与 Post 请求&lt;/h2&gt;
&lt;h4&gt;HTTP请求:&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;前端与后端的数据传递是通过 HTTP 请求实现的, 浏览器(前端)向服务器(后端)发送一个 HTTP 请求, 后端对请求进行处理, 然后再给浏览器发一个响应, 这就是 HTTP 的基本概念.&lt;/p&gt;
&lt;p&gt;get 和 post 是HTTP请求的两种方式, 最明显的区别是传递参数.&lt;/p&gt;
&lt;p&gt;如果你不大了解 HTTP 请求, 可以先查看文章末对 HTTP 的简述.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Get:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;关于 Get 请求&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;get 一般用来获取数据, 因为其本意就是获取. 浏览器访问一个页面时, 发送的第一个请求就是 Get.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;get 的参数将直接写明在请求 url 中, 这意味着, 任何人可以直接通过浏览器上方的地址来清除的看到, 例如你要下载一个东西, 文件名叫 &apos;idea&apos;, 将文件名作为filename参数传递过去, 那么这个请求类似于这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://xxx.com/download/?filename=idea
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在url中, 写一个&apos;&apos;?&apos;表示后面的内容是参数, 然后以 参数名=参数值 的格式表示参数, 如果要传多个参数, 则使用 &apos;&amp;amp;&apos; 连接它们&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务端接收到你的请求后, 可以根据参数来做出对应处理, 然后发送合适的响应.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一个例子:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在浏览器新建选项卡, 在url中输入一个地址, 发送的是一个get请求, 所以我们可以直接通过这个来测试.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;hitokoto 是一个提供&apos;一句话&apos;的网站, 提供了接口以供开发这调用, 其中一个是:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;地址&lt;/th&gt;
&lt;th&gt;协议&lt;/th&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v1.hitokoto.cn&lt;/td&gt;
&lt;td&gt;HTTPS&lt;/td&gt;
&lt;td&gt;ANY&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其中一个参数是:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;th&gt;可选&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;encode&lt;/td&gt;
&lt;td&gt;text / json / js&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;返回的编码&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;方法是Any, 即, 同时支持 get 与 post, 那么我们尝试使它返回一个格式为文本的结果, 对应的 url 是:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://v1.hitokoto.cn/?encode=text
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;hitokoto 服务器返回了我们所需要的值:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210105165205507.png&quot; alt=&quot;hitokoto返回的纯文本数据&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 那么, 如果是json呢? 试试看吧:
 
 ```
 https://v1.hitokoto.cn/?encode=json
 ```
 
 hitokoto 服务器又返回了我们所需要的值:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210105165331960.png&quot; alt=&quot;hitokoto返回的json数据&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Post:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;关于 Post 请求&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Post 有 &apos;邮递,布置&apos; 的意思, 一般用来将数据提交给服务端.&lt;/li&gt;
&lt;li&gt;看完 Get, 你也知道了 Get 的一个特点: 参数直接暴露在 url 中, 直接被别人看到, 肯定是不大妥当的, 假如你要向服务器提交你的密码, 让别人看到, 那就不好了, 而 Post 相对 Get 来说, 就稍微安全点了, 因为参数在这个HTTP请求体中.
&amp;lt;br/&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个POST请求的概述:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210110114706246.png&quot; alt=&quot;一个POST请求的概述:&quot; /&gt;
它的响应内容:
&lt;img src=&quot;/images/20210110114759960.png&quot; alt=&quot;一个POST请求的响应内容&quot; /&gt;
由此可知, POST可以大致认为, 只是将参数放到了请求体中, 相对来说, 严谨一些.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个例子:&lt;/p&gt;
&lt;p&gt;因为大多数浏览器不能直接发送 post 请求, 所以我们通过 python 与一个测试网页来测试:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;有道有一个翻译接口示例, 支持 post 请求&lt;/p&gt;
&lt;p&gt;地址: &apos;https://aidemo.youdao.com/trans&apos;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;介绍&lt;/th&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;q&lt;/td&gt;
&lt;td&gt;翻译文本&lt;/td&gt;
&lt;td&gt;要进行翻译的文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;from&lt;/td&gt;
&lt;td&gt;从哪种语言翻译&lt;/td&gt;
&lt;td&gt;auto / zh-CHS / en / 其他&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;to&lt;/td&gt;
&lt;td&gt;翻译到哪种语言&lt;/td&gt;
&lt;td&gt;auto / zh-CHS / en / 其他&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;Python:&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Python 可以通过 requests 包来发送 post 请求.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编写代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests

url = &quot;https://aidemo.youdao.com/trans&quot;
args = {&quot;q&quot;: &quot;翻译这段话&quot;, &quot;from&quot;: &quot;auto&quot;, &quot;to&quot;: &quot;auto&quot;}

resp = requests.post(url, args)  # 发送请求, 并将响应赋值到resp
print(resp.text)                 # 打印响应解码后的文本
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行后, 可以看到, 返回了我们需要的数据:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210105165515493.png&quot; alt=&quot;python发送post请求&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;编写一个简单的网页来测试:&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在HTML中, 可以通过 form 表单, 可以发送 get 和 post 请求.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编写代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;title&amp;gt;Post Request Demo&amp;lt;/title&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;form action=&quot;https://aidemo.youdao.com/trans&quot; method=&quot;post&quot; target=&quot;_blank&quot;&amp;gt;
            &amp;lt;label for=&quot;kwd&quot;&amp;gt;关键字:&amp;lt;/label&amp;gt;
            &amp;lt;input id=&quot;kwd&quot; type=&quot;text&quot; name=&quot;q&quot; placeholder=&quot;输入关键词&quot;/&amp;gt;
            &amp;lt;input type=&quot;hidden&quot; name=&quot;from&quot; value=&quot;auto&quot;/&amp;gt;
            &amp;lt;input type=&quot;hidden&quot; name=&quot;to&quot; value=&quot;auto&quot;/&amp;gt;
            &amp;lt;input type=&quot;submit&quot; value=&quot;请求&quot;/&amp;gt;
        &amp;lt;/form&amp;gt;
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;效果:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210105165549904.png&quot; alt=&quot;post请求测试demo&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 4. 输入 &quot;白嫖有道翻译API真棒&quot; 并按下请求按钮:
 
    返回了我们所需要的数据:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/20210105165649711.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;指正:&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;可能会有人告诉你, get 比 post 安全, 但事实上, 这仅仅是针对门外汉, 你只需要打开浏览器调试功能, 就可以看到这个页面的一切请求以及请求的各种详细内容, post 的参数也能看到.
所以说, 说 post 安全, 只是指它的参数不能直接在 url 中看到, 但只要你想看, 只需要打开调试...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;HTTP 概述:&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;一个HTTP数据包有头和体两部分, 头中包含了一些摘要数据, 这些数据以键值对的形式存放, 例如指定可接收的数据类型, 指定编码, 压缩算法, 数据长度等. 体中是这个包的主要内容, 例如传输文件时, 文件内容就位于体中. 如果想要详细的了解HTTP数据的内容, 不妨在浏览器中按下F12, 打开调试, 进入 &apos;网络(Network)&apos; 选项卡, 刷新页面, 然后观察一个个的数据包.&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>[C#] TOC Tiny - 基于TCP套接字简易网络聊天室</title><link>https://slimenull.com/posts/20201231010808/</link><guid isPermaLink="true">https://slimenull.com/posts/20201231010808/</guid><description>简介:之前开了一个大坑, 额, 其实就是带有注册登录, 然后完美解决粘包, 心跳包, 还有并发量等问题的坑, 但是太难填了(我太菜了), 于是我就开了一个新项目, 砍掉登陆注册功能, 直接作为聊天室开放…程序美照:难题是如何解决的:首先是传输协议, 我这里的数据传输, 无论是什么数据, 都是一个TransPackage实例, 里面有基本的结构, 就是Name, Content, ClientGuid, PackageType这四个字段(除此之外就没了). 然后把它们弄成字符串, 然后弄</description><pubDate>Thu, 31 Dec 2020 01:08:08 GMT</pubDate><content:encoded>&lt;h2&gt;简介:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;之前开了一个大坑, 额, 其实就是带有注册登录, 然后完美解决粘包, 心跳包, 还有并发量等问题的坑, 但是太难填了(我太菜了), 于是我就开了一个新项目, 砍掉登陆注册功能, 直接作为聊天室开放...&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;预览:&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/20201231004509995.png&quot; alt=&quot;客户端连接界面&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20201231004532309.png&quot; alt=&quot;客户端主页面&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20201231004603771.png&quot; alt=&quot;添加有颜文字&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20201231004652925.png&quot; alt=&quot;服务端&quot; /&gt;
&lt;strong&gt;难题是如何解决的&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先是传输协议, 我这里的数据传输, 无论是什么数据, 都是一个TransPackage实例, 里面有基本的结构, 就是Name, Content, ClientGuid, PackageType这四个字段(除此之外就没了). 然后把它们弄成字符串, 然后弄成字符数组, 然后发送.&lt;/li&gt;
&lt;li&gt;关于粘包, 也就是两个包黏在一起, 我这里使用的是自己封装好的事件驱动的套接字, 粘包问题发生时, 就是在一次收到消息时, 收到了两个或以上的包, 而每个包都是一个Json数据, Json数据的特点呢, 就是很容易能分开, 例如你Object并列, 没有分隔符, 也能明确的知道这是两个Object, 于是我又为我自己造的轮子 &lt;a href=&quot;/posts/20201028235244/&quot;&gt;CHO.Json&lt;/a&gt; 优化了一波, 使它支持这个功能&lt;/li&gt;
&lt;li&gt;关于并发, 其实这个问题在你发现TCP时全双工的时候就不需要考虑了, 因为是聊天室, 所以也不必担心什么. 之所以我考虑这个, 是因为我在很久之前写的聊天小程序, 由于我把TCP当成半双工用, 导致不能太过频繁的处理消息, 否则就可能会嗝屁, 但这次有我封装好的事件套接字, 就方便多了&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;更新记录:&lt;/h2&gt;
&lt;p&gt;最新版已经支持发送图片, 以及对图片的查看(旋转缩放移动)&lt;/p&gt;
&lt;h2&gt;下载链接：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://download.csdn.net/download/m0_46555380/13987649&quot;&gt;CSDN下载&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[C#] 控制台动态输入 - 增强版Console.ReadLine(), 在ReadLine的过程中获取用户已经输入的内容或移动已经输入内容的位置</title><link>https://slimenull.com/posts/20201230223307/</link><guid isPermaLink="true">https://slimenull.com/posts/20201230223307/</guid><description>简介：这是一个类库，正如标题所说，它具有这两个最基本而又强大的功能，有时候，我们可能会需要在ReadLine的过程中就访问已经输入了的内容，但.NET又没有提供这样的功能。其实在之前已经写过一个文章，也是动态输入，但是太烂了地址：旧的动态输入功能：在ReadLine的时候就读取已经输入了的内容，提供了完整的封装移动已经输入了的内容，你可以在输入时就将输入内容移动到控制台的任意位置光标移动，插入和覆盖模式，HOME和END键的处理。字符输入事件，在用户按下后，会有两个事件触发，可以通过</description><pubDate>Wed, 30 Dec 2020 22:33:07 GMT</pubDate><content:encoded>&lt;h2&gt;简介：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;这是一个类库，正如标题所说，它具有这两个最基本而又强大的功能，有时候，我们可能会需要在ReadLine的过程中就访问已经输入了的内容，但.NET又没有提供这样的功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;其实在之前已经写过一个文章，也是动态输入，但是太烂了地址：&lt;a href=&quot;/posts/20200406034849/&quot;&gt;旧的动态输入&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;功能：&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在ReadLine的时候就读取已经输入了的内容，提供了完整的封装&lt;/li&gt;
&lt;li&gt;移动已经输入了的内容，你可以在输入时就将输入内容移动到控制台的任意位置&lt;/li&gt;
&lt;li&gt;光标移动，插入和覆盖模式，HOME和END键的处理。&lt;/li&gt;
&lt;li&gt;字符输入事件，在用户按下后，会有两个事件触发，可以通过这两个事件来过滤用户输入内容，例如，仅允许输入数字，只需要判断事件参数即可。&lt;/li&gt;
&lt;li&gt;密码输入模式，与Linux的密码输入一致，不会显示任何内容，但功能按键以及输入事件仍然可用&lt;/li&gt;
&lt;li&gt;历史记录功能，它模拟的是Linux系统的命令行输入历史记录功能，比Windows的历史记录功能好那么一丢丢。&lt;/li&gt;
&lt;li&gt;（后续会增加更多功能，如果我需要或者你需要的话。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;原理：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;其实就是对 Console.ReadKey() 的妙用&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;使用方式：&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;实例化一个DynamicScanner对象。&lt;/li&gt;
&lt;li&gt;调用DynamicScanner的ReadLine()方法或者QuietReadLine()方法&lt;/li&gt;
&lt;li&gt;读取正在输入的内容，请使用类实例的InputtingString属性&lt;/li&gt;
&lt;li&gt;获取已输入内容的起始位置以及末端位置, 可使用StartLeft, StartTop, EndLeft, EndTop属性&lt;/li&gt;
&lt;li&gt;CharInput事件是当字符输入并且已经录入后触发的，PreviewCharInput是字符已经输入，但是并未录入的事件，CharInput要求返回值指定是否停止输入，PreviewCharInput要求返回值指定是否取消录入&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;如果代码有部分可以优化的部分，或者你有好的点子，欢迎私信我哦~
2021 / 1 / 5: 更新了源代码, 优化了一个小细节: 当此次输入为空时, 不保存历史记录&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;源代码：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;创建一个名为Null.DynamicScanner.cs的文件，粘贴以下内容，添加到你的项目中，即可使用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Null.Library
{
    public class DynamicScanner
    {
        int startTop, startLeft, endTop, endLeft, currentTop, currentLeft, tempEndTop, tempEndLeft;
        int inputIndex, historyIndex;
        bool insertMode = true, inputting = false, cursorVisible;
        private readonly object printting = false;                // 用于互斥锁
        ConsoleKeyInfo readedKey;
        private readonly List&amp;lt;List&amp;lt;char&amp;gt;&amp;gt; inputHistory;
        private List&amp;lt;char&amp;gt; inputtingChars;
        private string promptText = string.Empty;


        public delegate bool CharInputEventHandler(DynamicScanner sender, ConsoleKeyInfo c);
        public delegate bool PreviewCharInputEventHandler(DynamicScanner sender, ConsoleKeyInfo c);

        public event CharInputEventHandler CharInput;
        public event PreviewCharInputEventHandler PreviewCharInput;

        public DynamicScanner()
        {
            inputHistory = new List&amp;lt;List&amp;lt;char&amp;gt;&amp;gt;();
        }
        public string InputtingString
        {
            get
            {
                return inputtingChars == null ? string.Empty : new string(inputtingChars.ToArray());
            }
        }

        public int StartTop { get =&amp;gt; startTop; }
        public int StartLeft { get =&amp;gt; startLeft; }
        public int EndTop { get =&amp;gt; endTop; }
        public int EndLeft { get =&amp;gt; endLeft; }
        public int CurrentTop { get =&amp;gt; currentTop; }
        public int CurrentLeft { get =&amp;gt; currentLeft; }
        public bool IsInputting { get =&amp;gt; inputting; }
        public string PromptText { get =&amp;gt; promptText; set =&amp;gt; promptText = value; }

        public static bool IsControlKey(ConsoleKey k)
        {
            return k == ConsoleKey.Enter ||
                k == ConsoleKey.UpArrow ||
                k == ConsoleKey.DownArrow ||
                k == ConsoleKey.LeftArrow ||
                k == ConsoleKey.RightArrow ||
                k == ConsoleKey.Insert ||
                k == ConsoleKey.Backspace ||
                k == ConsoleKey.Delete ||
                k == ConsoleKey.Home ||
                k == ConsoleKey.End;
        }

        private void InitReadLine()
        {
            startTop = Console.CursorTop;
            startLeft = Console.CursorLeft;
            inputIndex = 0;

            if (inputHistory.Count == 0 || inputHistory[inputHistory.Count - 1].Count != 0)
            {
                historyIndex = inputHistory.Count;
                inputHistory.Add(new List&amp;lt;char&amp;gt;());
            }
            else
            {
                historyIndex = inputHistory.Count - 1;
            }

            inputtingChars = inputHistory[historyIndex];

            inputting = true;
        }
        private bool DealInputChar()
        {
            switch (readedKey.Key)
            {
                case ConsoleKey.Enter:
                    Console.WriteLine();
                    return true;
                case ConsoleKey.UpArrow:
                    if (historyIndex &amp;gt; 0)
                    {
                        historyIndex--;
                        UpdateInputState();
                    }
                    break;
                case ConsoleKey.DownArrow:
                    if (historyIndex &amp;lt; inputHistory.Count - 1)
                    {
                        historyIndex++;
                        UpdateInputState();
                    }
                    break;
                case ConsoleKey.LeftArrow:
                    if (inputIndex &amp;gt; 0)
                    {
                        inputIndex--;
                    }
                    break;
                case ConsoleKey.RightArrow:
                    if (inputIndex &amp;lt; inputtingChars.Count)
                    {
                        inputIndex++;
                    }
                    break;
                case ConsoleKey.Insert:
                    insertMode = !insertMode;
                    break;
                case ConsoleKey.Backspace:
                    if (inputIndex &amp;gt; 0)
                    {
                        inputtingChars.RemoveAt(inputIndex - 1);
                        inputIndex--;
                    }
                    break;
                case ConsoleKey.Delete:
                    if (inputIndex &amp;lt; inputtingChars.Count)
                    {
                        inputtingChars.RemoveAt(inputIndex);
                    }
                    break;
                case ConsoleKey.Home:
                    inputIndex = 0;
                    break;
                case ConsoleKey.End:
                    inputIndex = inputtingChars.Count;
                    break;
                default:
                    if (inputIndex == inputtingChars.Count)
                    {
                        inputtingChars.Add(readedKey.KeyChar);
                    }
                    else
                    {
                        if (insertMode)
                        {
                            inputtingChars.Insert(inputIndex, readedKey.KeyChar);
                        }
                        else
                        {
                            inputtingChars[inputIndex] = readedKey.KeyChar;
                        }
                    }
                    inputIndex++;
                    break;
            }
            return false;
        }
        private void PrintInputString()
        {
            lock(printting)
            {
                cursorVisible = Console.CursorVisible;
                Console.CursorVisible = false;
                Console.SetCursorPosition(startLeft, startTop);

                Console.Write(promptText);

                if (inputIndex == inputtingChars.Count)
                {
                    for (int i = 0; i &amp;lt; inputtingChars.Count; i++)
                    {
                        Console.Write(inputtingChars[i]);
                    }
                    currentLeft = Console.CursorLeft;
                    currentTop = Console.CursorTop;
                }
                else
                {
                    for (int i = 0; i &amp;lt; inputtingChars.Count; i++)
                    {
                        if (inputIndex == i)
                        {
                            currentLeft = Console.CursorLeft;
                            currentTop = Console.CursorTop;
                        }
                        Console.Write(inputtingChars[i]);
                    }
                }

                tempEndLeft = Console.CursorLeft;
                tempEndTop = Console.CursorTop;

                while (Console.CursorTop &amp;lt; endTop || Console.CursorLeft &amp;lt; endLeft)
                {
                    Console.Write(&apos; &apos;);
                }

                endLeft = tempEndLeft;
                endTop = tempEndTop;

                Console.SetCursorPosition(currentLeft, currentTop);
                Console.CursorVisible = cursorVisible;
            }
        }
        private void UpdateInputState()
        {
            inputtingChars = inputHistory[historyIndex];
            inputIndex = inputtingChars.Count;
        }
        public void ClearDisplayBuffer()
        {
            lock(printting)
            {
                cursorVisible = Console.CursorVisible;
                Console.CursorVisible = false;
                Console.SetCursorPosition(startLeft, startTop);
                while (Console.CursorTop &amp;lt; endTop || Console.CursorLeft &amp;lt; endLeft)
                {
                    Console.Write(&apos; &apos;);
                }
                Console.SetCursorPosition(startLeft, startTop);
                Console.CursorVisible = cursorVisible;
            }
        }
        public void DisplayTo(int cursorLeft, int cursorTop)
        {
            lock(printting)
            {
                cursorVisible = Console.CursorVisible;
                Console.CursorVisible = false;
                Console.SetCursorPosition(cursorLeft, cursorTop);
                startLeft = cursorLeft; startTop = cursorTop;

                Console.Write(promptText);

                if (inputIndex == inputtingChars.Count)
                {
                    for (int i = 0; i &amp;lt; inputtingChars.Count; i++)
                    {
                        Console.Write(inputtingChars[i]);
                    }
                    currentLeft = Console.CursorLeft;
                    currentTop = Console.CursorTop;
                }
                else
                {
                    for (int i = 0; i &amp;lt; inputtingChars.Count; i++)
                    {
                        if (inputIndex == i)
                        {
                            currentLeft = Console.CursorLeft;
                            currentTop = Console.CursorTop;
                        }
                        Console.Write(inputtingChars[i]);
                    }
                }

                endLeft = Console.CursorLeft;
                endTop = Console.CursorTop;
                Console.CursorVisible = cursorVisible;
            }
        }
        public void SetInputStart(int cursorLeft, int cursorTop)
        {
            lock(printting)
            {
                cursorVisible = Console.CursorVisible;
                Console.CursorVisible = false;
                Console.SetCursorPosition(startLeft, startTop);
                while (Console.CursorTop &amp;lt; endTop || Console.CursorLeft &amp;lt; endLeft)
                {
                    Console.Write(&apos; &apos;);
                }
                Console.SetCursorPosition(cursorLeft, cursorTop);
                startLeft = cursorLeft; startTop = cursorTop;

                Console.Write(promptText);

                if (inputIndex == inputtingChars.Count)
                {
                    for (int i = 0; i &amp;lt; inputtingChars.Count; i++)
                    {
                        Console.Write(inputtingChars[i]);
                    }
                    currentLeft = Console.CursorLeft;
                    currentTop = Console.CursorTop;
                }
                else
                {
                    for (int i = 0; i &amp;lt; inputtingChars.Count; i++)
                    {
                        if (inputIndex == i)
                        {
                            currentLeft = Console.CursorLeft;
                            currentTop = Console.CursorTop;
                        }
                        Console.Write(inputtingChars[i]);
                    }
                }

                endLeft = Console.CursorLeft;
                endTop = Console.CursorTop;
                Console.CursorVisible = cursorVisible;
            }
        }
        public string ReadLine()
        {
            InitReadLine();
            PrintInputString();

            while (true)
            {
                readedKey = Console.ReadKey(true);
                if (PreviewCharInput != null &amp;amp;&amp;amp; PreviewCharInput.Invoke(this, readedKey))
                {
                    continue;
                }

                if (DealInputChar())
                {
                    inputting = false;
                    return InputtingString;
                }

                PrintInputString();

                if (CharInput != null &amp;amp;&amp;amp; CharInput.Invoke(this, readedKey))
                {
                    inputting = false;
                    return InputtingString;
                }
            }
        }
        public string QuietReadLine()
        {
            InitReadLine();
            PrintInputString();

            while (true)
            {
                readedKey = Console.ReadKey(true);
                if (PreviewCharInput != null &amp;amp;&amp;amp; PreviewCharInput.Invoke(this, readedKey))
                {
                    continue;
                }

                if (DealInputChar())
                {
                    inputting = false;
                    return InputtingString;
                }

                if (CharInput != null &amp;amp;&amp;amp; CharInput.Invoke(this, readedKey))
                {
                    inputting = false;
                    return InputtingString;
                }
            }
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用实例：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;这是一个可以过滤输入的使用实例, 它仅允许用户输入数字, 并且支持使用WSAD按键控制已输入内容在控制台中的移动&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;using System;
using Null.Library;

namespace LibraryTest
{
    class Program
    {
        static void Main(string[] args)
        {
            DynamicScanner scanner = new DynamicScanner();
            scanner.PreviewCharInput += Scanner_PreviewCharInput;
            scanner.CharInput += Scanner_CharInput;
            while(true)
            {
                string temp = scanner.ReadLine();
            }
        }

        private static bool Scanner_CharInput(DynamicScanner sender, ConsoleKeyInfo c)
        {
            Console.Title = $&quot;Length: {sender.InputtingString.Length}, Inputed: {sender.InputtingString}&quot;;
            return false;
        }

        private static bool Scanner_PreviewCharInput(DynamicScanner sender, ConsoleKeyInfo c)
        {
            if (c.KeyChar &amp;gt;= &apos;0&apos; &amp;amp;&amp;amp; c.KeyChar &amp;lt;= &apos;9&apos; || DynamicScanner.IsControlKey(c.Key))
            {
                return false;       // 表示不取消, 即:录入这个字符
            }
            else
            {
                switch (c.Key)       // 通过WSAD按键可以控制输入内容移动
                {
                    case ConsoleKey.W:
                        sender.SetInputStart(sender.StartLeft, sender.StartTop - 1);
                        break;
                    case ConsoleKey.S:
                        sender.SetInputStart(sender.StartLeft, sender.StartTop + 1);
                        break;
                    case ConsoleKey.A:
                        sender.SetInputStart(sender.StartLeft - 1, sender.StartTop);
                        break;
                    case ConsoleKey.D:
                        sender.SetInputStart(sender.StartLeft + 1, sender.StartTop);
                        break;
                }
                return true;
            }
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你觉得这些内容对你有帮助， 请一定要点个赞，或者收藏它，这是对我莫大的鼓励
也欢迎私信我，交个朋友也是非常棒的~&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>[Linux] 执行但不阻塞 以‘后台‘模式启动程序或脚本</title><link>https://slimenull.com/posts/20201218011610/</link><guid isPermaLink="true">https://slimenull.com/posts/20201218011610/</guid><description>1. 使用 ‘&amp;’ 符号：例如启动一个脚本，执行 ‘./idea.sh’ 以启动idea，但终端会被阻塞，若终端被关闭，idea也就被关闭。此时，执行 ‘./idea.sh &amp;’ 可以使 idea 脱离当前终端运行，即便当前终端被关闭，idea也不会受影响。./idea.sh     # 会阻塞当前终端进程./idea.sh &amp;   # 不会阻塞2. 使用 ‘nohup’ 指令：参考 菜鸟教程 - nohup指令参考文章：Linux后台执行命令(非阻塞式)</description><pubDate>Fri, 18 Dec 2020 01:16:10 GMT</pubDate><content:encoded>&lt;h2&gt;1. 使用 &apos;&amp;amp;&apos; 符号：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;例如启动一个脚本，执行 &apos;./idea.sh&apos; 以启动idea，但终端会被阻塞，若终端被关闭，idea也就被关闭。&lt;/li&gt;
&lt;li&gt;此时，执行 &apos;./idea.sh &amp;amp;&apos; 可以使 idea 脱离当前终端运行，即便当前终端被关闭，idea也不会受影响。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;./idea.sh     # 会阻塞当前终端进程
./idea.sh &amp;amp;   # 不会阻塞
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 使用 &apos;nohup&apos; 指令：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;参考 &lt;a href=&quot;https://www.runoob.com/linux/linux-comm-nohup.html&quot;&gt;菜鸟教程 - nohup指令&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：&lt;a href=&quot;https://blog.p2hp.com/archives/5528&quot;&gt;Linux后台执行命令(非阻塞式)&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Ubuntu 新手引导 - 中文输入法配置</title><link>https://slimenull.com/posts/20201217233937/</link><guid isPermaLink="true">https://slimenull.com/posts/20201217233937/</guid><description>选择输入法框架：Linux中，有多个输入法框架可供使用，在 Ubuntu 中，默认预装了 ibus 这款输入法引擎，常用的中文输入法引擎有两种，ibus 与 fcitx。两者都还彳亍，懒得下载的话，就直接用ibus吧。配置使用的输入法框架：打开终端，执行 ‘im-config’，在弹出的窗口中，选择ok，yes，然后选择 ibus，于是，你就成功使用了ibus框架。同理，选择 fcitx 则是使用 fcitx 框架，至于安装 fcitx，见文章末。选择输入法引擎：关键的时刻到咯，那么对于ibu</description><pubDate>Thu, 17 Dec 2020 23:39:37 GMT</pubDate><content:encoded>&lt;h2&gt;选择输入法框架：&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Linux中，有多个输入法框架可供使用，在 Ubuntu 中，默认预装了 ibus 这款输入法引擎，常用的中文输入法引擎有两种，ibus 与 fcitx。两者都还彳亍，懒得下载的话，就直接用ibus吧。&lt;/li&gt;
&lt;li&gt;配置使用的输入法框架：打开终端，执行 &apos;im-config&apos;，在弹出的窗口中，选择ok，yes，然后选择 ibus，于是，你就成功使用了ibus框架。同理，选择 fcitx 则是使用 fcitx 框架，至于安装 fcitx，见文章末。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;选择输入法引擎：&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;关键的时刻到咯，那么对于ibus，常用的输入法有：rime，sunpinyin，googlepinyin(谷歌拼音)，我的话，推荐rime和sunpinyin。&lt;/li&gt;
&lt;li&gt;对于fcitx嘛，常用的有sogoupinyin(搜狗拼音)，rime，sunpinyin，googlepinyin，搜狗的话，其实还是很棒的(没有广告，毕竟谁会往Linux系统软件中投放广告呢,,,)，然后rime和sunpinyin，也都蛮棒咯。&lt;/li&gt;
&lt;li&gt;至于安装，见文章末。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;添加输入法引擎：&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;ibus的话，打开终端，执行 &apos;ibus-setup&apos;，然后在 &apos;Input method&apos; 选项卡中就可以添加了。（什么？不会操作？建议重修英文）&lt;/li&gt;
&lt;li&gt;fcitx的话，更简单了，打开 Ubuntu 应用列表(我指屏幕左下角的按钮)，搜索fcitx，打开fcitx configuration，然后，你懂得，添加输入法就好。实在不会，参考这篇文章吧 &lt;a href=&quot;https://editor.csdn.net/md/?articleId=110823818&quot;&gt;在Kali中配置中文输入法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;最后一点，如果你在添加输入法引擎的时候，找不到自己安装的输入法，先确认自己的输入法框架是否选择正确了，也就是在 &apos;im-config&apos; 中查看，二就是尝试重启。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;添加输入法源：&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;重启计算机以保证上面的更改生效，否则无法添加输入法源。&lt;/li&gt;
&lt;li&gt;打开Settings（设置）&lt;/li&gt;
&lt;li&gt;找到 Region &amp;amp; Language (地区和语言)。&lt;/li&gt;
&lt;li&gt;在 Input Source (输入源) 中点击 &apos;+&apos; 添加输入源，选择自己安装的输入法。它一般位于 Chinese (汉语) 组中，例如 &apos;Chinese (SunPinyin)&apos;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;再次强调，如果你找不到，就尝试重启&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/20201217231817146.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;享受打字：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;如果你走的是 ibus 线，那么现在，你现在可以通过 Super + Space(空格) 来切换输入方式了。&lt;/li&gt;
&lt;li&gt;如果你走的是 fcitx 线，则是通过 Ctrl + Space(空格) 来切换输入法，并且你可以在 fcitx configuration 中设置甚至添加更换输入法的快捷键。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;在 Ubuntu 中， Super 键对应着 Windows 徽标键&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h5&gt;安装输入法框架：&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;哦？不会安装了吗？让我来帮助你吧。&lt;/li&gt;
&lt;li&gt;安装 ibus 使用 &apos;sudo apt install ibus&apos; 指令即可。&lt;/li&gt;
&lt;li&gt;安装 fcitx 使用 &apos;sudo apt install fcitx&apos; 即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;下载速度慢？ 如果不是网络问题的话，就尝试更换软件安装源吧。这个在网络上可以很容易的搜索到。个人建议 阿里云镜像 以及 华为云镜像。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;安装输入法引擎：&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;其实这件事情也蛮简单的，因为 apt 可以搜索软件，通过 &apos;apt search ibus&apos; 就可以搜索到关于 ibus 的软件，其中不乏有许多 ibus 的输入法引擎&lt;/li&gt;
&lt;li&gt;输入法引擎的命名方式很简单，就是 框架名-引擎名，例如 ibus-rime，直接 &apos;sudo apt install ibus-rime&apos; 即可安装适用于 ibus 的 rime 输入法。本文章提到的其他输入法引擎也是如此(只有sogoupinyin需要在&lt;a href=&quot;https://pinyin.sogou.com/linux/&quot;&gt;SogouPinyin官网&lt;/a&gt;下载)&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;其他小问题：&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;不知为何，Ubuntu中 ibus 有点问题，它无法设置为候选词水平显示，网络上也找不到解决方式，如果你介意这一点，那么就使用fcitx框架吧。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;&lt;em&gt;作者说:&lt;/em&gt;&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;如果可以的话，为此文章点赞，并且加入到写文章的行列，国内IT圈环境需要我们做出贡献！&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>「干货」 装机如此简单！Windows 与 Linux 通用装机方法。(无需VM虚拟机)</title><link>https://slimenull.com/posts/20201213001156/</link><guid isPermaLink="true">https://slimenull.com/posts/20201213001156/</guid><description>Windows 与 Linux 通用装机方法步骤概述：准备安装介质从你喜欢的渠道下载ISO镜像将ISO写入到安装介质步骤：1. 准备安装介质旧时的安装介质指光盘，但现在，一般的，安装介质都指U盘。你需要准备一个U盘，这个U盘的数据将被完全擦除然后写入安装程序，所以，推荐使用空闲U盘，并且空间大小推荐8G左右，不要小于4G，可以大于8G(只不过有些浪费空间就是了)Windows10的镜像一般为4G左右，写入到U盘后大于4GLinux镜像一般较小，大多4G以下，但是Kali Linux的</description><pubDate>Sun, 13 Dec 2020 00:11:56 GMT</pubDate><content:encoded>&lt;h2&gt;Windows 与 Linux 通用装机方法&lt;/h2&gt;
&lt;h5&gt;步骤概述：&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;准备安装介质&lt;/li&gt;
&lt;li&gt;从你喜欢的渠道下载ISO镜像&lt;/li&gt;
&lt;li&gt;将ISO写入到安装介质&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;步骤：&lt;/h3&gt;
&lt;h5&gt;1. 准备安装介质&lt;/h5&gt;
&lt;p&gt;旧时的安装介质指光盘，但现在，一般的，安装介质都指U盘。你需要准备一个U盘，这个U盘的数据将被完全擦除然后写入安装程序，所以，推荐使用空闲U盘，并且空间大小推荐8G左右，不要小于4G，可以大于8G(只不过有些浪费空间就是了)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Windows10的镜像一般为4G左右，写入到U盘后大于4G
Linux镜像一般较小，大多4G以下，但是Kali Linux的镜像要大于4G
这就是为什么推荐8G U盘&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;2. 从你喜欢的渠道下载ISO镜像&lt;/h5&gt;
&lt;p&gt;如果你希望你安装的系统是干净的，那么，你需要使用正确的渠道来下载系统镜像。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装 Windows 系统：
&lt;ul&gt;
&lt;li&gt;如果你有激活密钥或可以使用数字激活，建议你去 &lt;strong&gt;&lt;a href=&quot;https://next.itellyou.cn/&quot;&gt;Next, I tell you&lt;/a&gt;&lt;/strong&gt; 下载镜像，你需要登录(这不会花费太长时间)，它也是支持QQ登录的，当然，如果你不想注册账户，也可以去旧的网站 &lt;strong&gt;&lt;a href=&quot;https://next.itellyou.cn/&quot;&gt;MSDN I tell you&lt;/a&gt;&lt;/strong&gt; ， 但在旧的网站，你无法下载Windows10的最新版本 &lt;em&gt;2004&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;如果你没有激活密钥, 你可以选择购买 Windows 激活码, 在 &lt;a href=&quot;https://www.microsoft.com/&quot;&gt;Microsoft 商城&lt;/a&gt; 或 &lt;a href=&quot;https://search.jd.com/Search?keyword=Windows%E6%BF%80%E6%B4%BB%E7%A0%81&quot;&gt;京东&lt;/a&gt;, &lt;a href=&quot;https://www.taobao.com/&quot;&gt;淘宝&lt;/a&gt; 等电子商城中可以购买到, 购买后, 按照上面的步骤, 下载 Windows 系统镜像. 或者你可以先进行上面的步骤, 在提到的网站中下载镜像, 然后试用 Windows. 如果你想要使用系统激活工具, 一定要知道, 这是不合法的, 并且激活工具常常伴随着病毒, 这些病毒很难被处理掉. &lt;s&gt;360或许可以搞定它们&lt;/s&gt;. 如果你想要使用盗版软件(不需要你为其支付费用), 请参阅文章尾 &lt;em&gt;硬盘安装&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;安装 Linux 系统：
&lt;ul&gt;
&lt;li&gt;不用讲了，Linux 的大部分发行版都是免费且开源，直接在官方网站即可下载到。&lt;a href=&quot;https://ubuntu.com/&quot;&gt;Ubuntu&lt;/a&gt;， &lt;a href=&quot;https://www.debian.org/&quot;&gt;Debian&lt;/a&gt;，&lt;a href=&quot;https://www.deepin.org/&quot;&gt;Deepin&lt;/a&gt;，&lt;a href=&quot;https://www.kali.org/&quot;&gt;Kali&lt;/a&gt;，&lt;a href=&quot;https://www.archlinux.org/&quot;&gt;Arch&lt;/a&gt;，&lt;a href=&quot;https://www.centos.org&quot;&gt;CentOS&lt;/a&gt;，&lt;a href=&quot;https://linuxmint.com&quot;&gt;Mint&lt;/a&gt;，&lt;a href=&quot;https://www.redhat.com/&quot;&gt;Red Hat&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;提示：I tell you网站内的ed2k是一个特殊的下载地址，可视作p2p下载链接，推荐使用&lt;a href=&quot;https://dl.xunlei.com/&quot;&gt;迅雷&lt;/a&gt;(别的软件可能对非技术人员不友好)
注意，大多数国内流行的ISO或GHO镜像下载网站都是&lt;strong&gt;包含捆绑软件&lt;/strong&gt;的&lt;strong&gt;非官方原装系统&lt;/strong&gt;！如果你要使用纯净系统，切记要仔细选择！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;3. 将ISO写入到介质&lt;/h5&gt;
&lt;p&gt;写入到介质可不单单是将ISO作为压缩包打开并内容拷贝到U盘，这样是错误的操作！但是不必担心，同样，它也不是什么复杂的操作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 Windows 系统中，你需要一个小工具来制作安装介质，它就是&lt;a href=&quot;http://rufus.ie/&quot;&gt;Rufus&lt;/a&gt;！， 一个仅有不到5MB的小工具，并且能够自动帮你检测可能缺失的内容！&lt;a href=&quot;http://rufus.ie/&quot;&gt;Rufus&lt;/a&gt;的使用也非常简便，只需要选择设备，选择ISO镜像，点击开始即可(我是指, 其他配置内容默认不需要更改)。&lt;a href=&quot;http://rufus.ie&quot;&gt;进入Rufus官网&lt;/a&gt;，&lt;a href=&quot;https://github.com/pbatard/rufus/releases/download/v3.13/rufus-3.13p.exe&quot;&gt;下载Rufus 3.13便携版&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;在Linux中，我们只需要借助一个‘dd’指令即可将ISO镜像写入到U盘，用法是：&lt;pre&gt;&lt;code&gt;dd if=‘输入文件’ of=‘输出设备’
# 使用lsblk指令可以查看自己当前的接入的存储设备
# dd指令示例使用示例： dd if=&apos;/home/username/ubuntu20.04lts.iso&apos; of=&apos;/dev/sda&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;在这里不得不提到一个问题，就是在 Linux 中使用 dd 指令写入 Windows 镜像到U盘，会出现无法安装的问题，但是使用 Rufus 写入就没问题。（可能是格式问题）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;示例图像&lt;/th&gt;
&lt;th&gt;简介&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;/images/20201212211500172.png&quot; alt=&quot;Rufus界面图像&quot; /&gt; | Rufus 的界面非常简洁, 仅有必要的选项，无任何附加功能，无论小白还是大神，都很适合使用&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20201212212942916.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;| 这是Ubuntu系统中，dd指令的帮助手册，不过对于只需要将ISO写入到U盘的我们，不需要看那么多内容，只需要知道if和of参数即可&lt;/p&gt;
&lt;h5&gt;4. 开始安装&lt;/h5&gt;
&lt;p&gt;待写入完毕，安装准备已经完全就绪了！那么，下一步就是安装步骤了！&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;关闭计算机&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;插入准备好的安装介质，启动计算机&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从U盘启动（有两种方式）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第一种：按下 &lt;em&gt;选择引导&lt;/em&gt; 的按键，它可能是 F12，但不同主板有不同的按键，Dell电脑的按键即是 F12。&lt;/li&gt;
&lt;li&gt;第二种：按下进入 &lt;em&gt;启动配置&lt;/em&gt; 的按键 ，在 &lt;em&gt;启动配置&lt;/em&gt; 中调整引导顺序至USB设备最顶级，重启电脑即可进入安装界面&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;若有疑问，可参阅这篇文章： &lt;a href=&quot;/posts/20201212233309/&quot;&gt;进入BIOS/UEFI配置页面&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;成功从U盘启动后，就已经进入到系统安装界面了。接下来，按照安装程序的提示进行即可。一般的，Linux系统的可定制性要比Windows高很多，而Windows的安装就简单了很多。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;享受新系统！&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;h4&gt;硬盘安装：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;某些ISO镜像，例如国内较为流行的镜像下载网站提供的ISO，将其中的内容直接解压到某处，启动 setup.exe 即可安装，这种安装方式被称为 &lt;em&gt;硬盘安装&lt;/em&gt; ，并且一般的，这种安装方式都是一键傻瓜式安装，中途完全不需要用户进行任何干涉。&lt;/li&gt;
&lt;li&gt;下面列举一些国内的非官方镜像下载源：
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://www.deepinghost.com/&quot;&gt;深度技术&lt;/a&gt;， &lt;a href=&quot;http://www.ylmfeng.com/&quot;&gt;雨林木风&lt;/a&gt;，&lt;a href=&quot;http://www.luobojiayuanxt.com/&quot;&gt;萝卜家园&lt;/a&gt;，&lt;a href=&quot;http://www.dngs.co/&quot;&gt;电脑公司&lt;/a&gt;，&lt;a href=&quot;http://www.xitongzhijia.net/&quot;&gt;系统之家&lt;/a&gt;，&lt;a href=&quot;http://www.winfanqie.com/&quot;&gt;番茄花园&lt;/a&gt;
&amp;lt;font color=&quot;#EE6666&quot;&amp;gt;&lt;strong&gt;注意，你几乎无法在国内流行镜像下载站获取原版镜像！这些都是包含捆绑的非原版镜像！并且极有可能侵犯了微软(Microsoft)的版权，在使用之前，慎重考虑！&lt;/strong&gt;&amp;lt;/font&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;在下载到支持硬盘安装的ISO镜像后，使用任意压缩包管理软件(例如&lt;a href=&quot;https://www.7-zip.org/&quot;&gt;7zip&lt;/a&gt;)打开，解压其中的内容到任意文件夹，但要求不可以在你要安装系统的分区，例如你要将系统安装到C盘，那就不可以将ISO文件解压到C盘。&lt;/li&gt;
&lt;li&gt;解压后，双击 setup.exe 按照自己的需要配置一下，如果看不懂，就直接点下一步，然后傻瓜式系统安装就开始了，解压来你需要做的就是等待安装了（喝杯Java提提神？&lt;/li&gt;
&lt;li&gt;安装完成后，享受新系统吧！（记得将所有捆绑的东西全部卸载&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[电脑小白入门] 如何进入启动配置 (BIOS/UEFI配置)</title><link>https://slimenull.com/posts/20201212233309/</link><guid isPermaLink="true">https://slimenull.com/posts/20201212233309/</guid><description>电脑启动时，需要读取最基本的配置，然后才进入系统，这个配置就是 启动配置 。旧时的启动配置即BIOS配置，但如今UEFI即将取代BIOS配置，我们应该使用 启动配置 这个名称，否则就可能闹出将UEFI称为BIOS的尴尬场面。这个启动配置，是可以被我们调整的，在启动配置页面，我们可以调整风扇转速，系统时间，设备电压，以及其他更高深的东西，不过最常用的，还是引导优先级。因为一个电脑，可以有多个操作系统，引导优先级则是控制启动系统的配置项。不同的主板，是有不同的进入按键的，例如Dell电脑，是F2进入启动..</description><pubDate>Sat, 12 Dec 2020 23:33:09 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;电脑启动时，需要读取最基本的配置，然后才进入系统，这个配置就是 &lt;em&gt;启动配置&lt;/em&gt; 。旧时的启动配置即BIOS配置，但如今UEFI即将取代BIOS配置，我们应该使用 &lt;em&gt;启动配置&lt;/em&gt; 这个名称，否则就可能闹出将UEFI称为BIOS的尴尬场面。&lt;/li&gt;
&lt;li&gt;这个启动配置，是可以被我们调整的，在启动配置页面，我们可以调整风扇转速，系统时间，设备电压，以及其他更高深的东西，不过最常用的，还是&lt;strong&gt;引导优先级&lt;/strong&gt;。因为一个电脑，可以有多个操作系统，引导优先级则是控制启动系统的配置项。&lt;/li&gt;
&lt;li&gt;不同的主板，是有不同的进入按键的，例如Dell电脑，是&amp;lt;kbd&amp;gt;F2&amp;lt;/kbd&amp;gt;进入启动配置。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;那么， 下一步就是在网上查找你的设备对应的按键了, 搜索近似于 ‘设备名 BIOS/UEFI 按键’ 的内容以尝试检索出你所需要的按键。下面的步骤以 &amp;lt;kbd&amp;gt;F2&amp;lt;/kbd&amp;gt;作为示例。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;按下开机键后，狂按&amp;lt;kbd&amp;gt;F2&amp;lt;/kbd&amp;gt;, 直到进入配置页面。（它可能是纯英文界面&lt;/li&gt;
&lt;li&gt;如果是国内主板，或者说支持多语言的配置页，你可以尝试找下 ‘Language’ 配置以调整显示语言&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;启动时，还有一些其他的按键，例如 &lt;em&gt;Windows启动高级选项&lt;/em&gt; 的按键是&amp;lt;kbd&amp;gt;F8&amp;lt;/kbd&amp;gt;，Dell电脑的引导选择的快捷键是&amp;lt;kbd&amp;gt;F12&amp;lt;/kbd&amp;gt;, 如果你只是希望在本次进入系统时从其他设备进入，使用&amp;lt;kbd&amp;gt;F12&amp;lt;/kbd&amp;gt;更简便些。当然，即便是这个，在不同主板中也是不同的，例如我那个台式机就是&amp;lt;kbd&amp;gt;F11&amp;lt;/kbd&amp;gt;。&lt;/p&gt;
&lt;p&gt;对于想要通过介质安装系统的朋友呢，在开机时狂按&amp;lt;kbd&amp;gt;F12&amp;lt;/kbd&amp;gt;进入引导选择页面，选择带有USB字样的选项并按&amp;lt;kbd&amp;gt;Enter&amp;lt;/kbd&amp;gt;即可进入安装程序。&lt;/p&gt;
</content:encoded></item><item><title>Kali Linux 新手引导 - Xfce桌面时间配置</title><link>https://slimenull.com/posts/20201209122311/</link><guid isPermaLink="true">https://slimenull.com/posts/20201209122311/</guid><description>关于我的Kali系统：操作系统：Linux NullKali 5.9.0-kali2-amd64 #1 SMP Debian 5.9.6-1kali1 (2020-11-11) x86_64 GNU/Linux桌面环境：Xfce Desktop Environment Version 4.14, destributed by Debian打开时间配置：位于固定’任务栏’的右侧， 右击时间，选择Properties(属性)进行调整：亦见：Kali Linux 新手引导 </description><pubDate>Wed, 09 Dec 2020 12:23:11 GMT</pubDate><content:encoded>&lt;h4&gt;关于我的Kali系统：&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;操作系统：Linux NullKali 5.9.0-kali2-amd64 #1 SMP Debian 5.9.6-1kali1 (2020-11-11) x86_64 GNU/Linux
桌面环境：Xfce Desktop Environment Version 4.14, destributed by Debian&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;打开时间配置：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;位于固定&apos;任务栏&apos;的右侧， 右击时间，选择Properties(属性)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;table&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td&amp;gt;
&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20201209115256539.png&quot; title=&quot;时间&quot;/&amp;gt;
&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/2020120911532581.png&quot;/&amp;gt;
&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;&lt;/p&gt;
&lt;h2&gt;进行调整：&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/20201209115711659.png&quot; alt=&quot;界面&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;亦见：&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;/posts/20201207165005/&quot;&gt;Kali Linux 新手引导 - 区域与语言配置&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Kali Linux 记录 - 死锁，依赖 ‘Unable to install new version of ...libpng12.so.0 no such file or directory‘</title><link>https://slimenull.com/posts/20201208195444/</link><guid isPermaLink="true">https://slimenull.com/posts/20201208195444/</guid><description>概述:在使用apt或dpkg安装deb包时， 出现了no such file or directory的错误要点:在dpkg时是否指定了错误的路径包对于系统来说是否过旧我的解决过程:我遇到的问题属于要点中的后者， libpng12对于我的系统来说太旧了。具体情况是这样的， 我在使用apt安装一些软件时， 发现， 一直报依赖问题， 让我执行‘apt --fix-broken install’(即‘apt -f install’)， 但当我执行时， 它尝试安装的libpng12-0始</description><pubDate>Tue, 08 Dec 2020 19:54:44 GMT</pubDate><content:encoded>&lt;h2&gt;概述:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在使用apt或dpkg安装deb包时， 出现了no such file or directory的错误&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;要点:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在dpkg时是否指定了错误的路径&lt;/li&gt;
&lt;li&gt;包对于系统来说是否过旧&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;我的解决过程:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;我遇到的问题属于要点中的后者， libpng12对于我的系统来说太旧了。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;具体情况是这样的， 我在使用apt安装一些软件时， 发现， 一直报依赖问题， 让我执行‘apt --fix-broken install’(即‘apt -f install’)， 但当我执行时， 它尝试安装的libpng12-0始终装不上(都快把孩子急哭了qaq)！ 从报错信息上看， libpng12-0似乎是libpng12.so.0的新版本， 在安装时总是提示no such file or directory。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;首先， 我在网上搜索了一波， 但是依照国内IT圈子转载满天飞的尿性， 我还是以失败告终 —— 找了几十篇文章也没看到有用的&lt;/li&gt;
&lt;li&gt;后来我发现， 如果我不处理这个错误， 我不能进行任何apt install， upgrade， remove操作！ 我慌了， 于是我尝试执行apt remove libpng12.so.0， 但是提示它被libwebkitgtk什么的一个我不认识的东西包所依赖， 然后， 不能移除。。。&lt;/li&gt;
&lt;li&gt;然后我又尝试移除这个依赖于libpng的libwebkitgtk的什么包， 然后我发现它又被sunloginclient(应该是向日葵远程控制软件客户端)所依赖，，， 移除失败，，，&lt;/li&gt;
&lt;li&gt;于是我尝试移除sunloginclient这个包（反正我目前不大用得到向日葵）， 但是，，，正如我前面所说的， 虽然sunloginclient不被什么包所依赖， 但是因为我们libpng12-0的依赖问题没解决， 所以，，， 移除失败。。。&lt;/li&gt;
&lt;li&gt;于是， 我似乎陷入了一个逻辑问题， 想要处理这个依赖问题， 我就得移除掉旧的libpng12.so.0， 想要移除它我就得移除sunloginclient然后最后移除它， 而只有我处理完依赖问题后， 才能够使用apt来进行安装与移除操作！&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;table align=&quot;center&quot;&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;绝了&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;&amp;lt;img align=&quot;center&quot; width=&quot;100&quot; height=&quot;100&quot; src=&quot;https://i0.hdslb.com/bfs/emote/35d62c496d1e4ea9e091243fa812866f5fecc101.png@112w_112h.png&quot;/&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/table&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;查资料， 查资料， 查资料， 经过一段时间的周旋， 我顿悟了， 很快啊， 我打开终端， 用dpkg把sunloginclient给移除了， 然后顺理成章的把libwebkitgtk， libpng12.so.0给移除了&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;完美解决&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Kali Linux 新手引导 - 配置中文输入法</title><link>https://slimenull.com/posts/20201207203524/</link><guid isPermaLink="true">https://slimenull.com/posts/20201207203524/</guid><description>关于我的Kali系统：操作系统：Linux NullKali 5.9.0-kali2-amd64 #1 SMP Debian 5.9.6-1kali1 (2020-11-11) x86_64 GNU/Linux桌面环境：Xfce Desktop Environment Version 4.14, destributed by Debian安装输入法框架Fcitxsudo apt install fcitx# kali默认是装了fcitx的， 如果没有安装， 就执行上边的指令安装吧安装好用的</description><pubDate>Mon, 07 Dec 2020 20:35:24 GMT</pubDate><content:encoded>&lt;h4&gt;关于我的Kali系统：&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;操作系统：Linux NullKali 5.9.0-kali2-amd64 #1 SMP Debian 5.9.6-1kali1 (2020-11-11) x86_64 GNU/Linux
桌面环境：Xfce Desktop Environment Version 4.14, destributed by Debian&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;安装输入法框架Fcitx&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install fcitx
# 这里推荐的是Fcitx， kali默认是装了fcitx的， 如果没有安装， 就执行上边的指令安装吧
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装好用的输入法&lt;/h2&gt;
&lt;p&gt;访问 ‘搜狗输入法 for Linux’ 官网并下载deb包。&lt;a href=&quot;https://pinyin.sogou.com/linux/&quot;&gt;点此处跳转&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dpkg -i sogoupinyin_版本号_amd64.deb
# 若出现依赖问题， 则执行指令以尝试修复: sudo apt -f install
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置输入法&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;启动 Fcitx Configuration ， 点击左下角加号以添加输入法， 取消勾选‘Only Show Current Language’， 找到sogoupinyiin （没错， 不是sougou而是sogou）
&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20201207180141330.png&quot;/&amp;gt;&lt;/li&gt;
&lt;li&gt;现在， 一切就绪， 你可以使用输入法了。 默认的输入法切换快捷键是 ‘Ctrl + Space’ ， 可在Fcitx Configuration的Global Config中调整。切换至‘搜狗拼音’时， 在屏幕上会出现在搜狗拼音的语言栏， 如下：
&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20201207180555556.png&quot;/&amp;gt;&lt;/li&gt;
&lt;li&gt;此时， 你可以正常使用输入法。若有其他问题， 可继续阅读下面的内容。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;table&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td&amp;gt;
&amp;lt;span&amp;gt;&amp;lt;font color=&quot;#888&quot;&amp;gt;下图即配置好的Fcitx&amp;lt;/font&amp;gt;&amp;lt;/span&amp;gt;
&amp;lt;img src=&quot;https://img-blog.csdnimg.cn/20201207180031275.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzQ2NTU1Mzgw,size_16,color_FFFFFF,t_70&quot;/&amp;gt;
&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;&lt;/p&gt;
&lt;h2&gt;玄学问题：&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;h6&gt;Fcitx Configuration 启动后， 窗口中没有任何内容（正常情况下应该有一个Keyboard - 语言）&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;尝试重启， 重新安装Fcitx， 更换区域与语言（参阅：&lt;a href=&quot;/posts/20201207165005/&quot;&gt;Kali Linux 新手引导 - 区域与语言配置&lt;/a&gt;）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;h6&gt;在Fcitx Configuration 中添加输入法时， 无法找到sogoupinyin&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;检查输入法是否正确安装了， 尝试执行apt update， apt upgrade， apt -f install&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Kali Linux 新手引导 - 区域与语言配置</title><link>https://slimenull.com/posts/20201207165005/</link><guid isPermaLink="true">https://slimenull.com/posts/20201207165005/</guid><description>关于我的Kali系统：操作系统：Linux NullKali 5.9.0-kali2-amd64 #1 SMP Debian 5.9.6-1kali1 (2020-11-11) x86_64 GNU/Linux桌面环境：Xfce Desktop Environment Version 4.14, destributed by Debian配置区域tzselect# 在Shell中执行即可， 内容来自网络配置语言sudo dpkg-reconfigure locales# 配置完后记得重</description><pubDate>Mon, 07 Dec 2020 16:50:05 GMT</pubDate><content:encoded>&lt;h4&gt;关于我的Kali系统：&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;操作系统：Linux NullKali 5.9.0-kali2-amd64 #1 SMP Debian 5.9.6-1kali1 (2020-11-11) x86_64 GNU/Linux
桌面环境：Xfce Desktop Environment Version 4.14, destributed by Debian&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;配置区域&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;tzselect
# 在Shell中执行即可， 内容来自网络
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置语言&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo dpkg-reconfigure locales
# 配置完后记得重启(reboot)
# 这条指令的意思应该是调整语言并安装相关包
# 配置完语言后，在登录页面的右上角可以快捷切换语言
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[Win32] Windows API 函数后缀(A,W,ExA,ExW)的作用</title><link>https://slimenull.com/posts/20201120033237/</link><guid isPermaLink="true">https://slimenull.com/posts/20201120033237/</guid><description>资料来自网络, 在此整理到一起A表示使用ANSI编码作为标准输入与输出流的文本编码W表示使用Unicode作为编码Ex表示拓展, 标注了Ex的winapi函数会比没有标Ex的函数多一些参数什么的, 可以说拓展了一些功能ExA 与 ExW 就是 A,W与Ex的结合了...</description><pubDate>Fri, 20 Nov 2020 03:32:37 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;资料来自网络, 在此整理到一起&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;A表示使用ANSI编码作为标准输入与输出流的文本编码&lt;/li&gt;
&lt;li&gt;W表示使用Unicode作为编码&lt;/li&gt;
&lt;li&gt;Ex表示拓展, 标注了Ex的winapi函数会比没有标Ex的函数多一些参数什么的, 可以说拓展了一些功能&lt;/li&gt;
&lt;li&gt;ExA 与 ExW 就是 A,W与Ex的结合了&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[C#] 在控制台绘图, 如:放置图像, 绘制线条</title><link>https://slimenull.com/posts/20201115215901/</link><guid isPermaLink="true">https://slimenull.com/posts/20201115215901/</guid><description>[C#] 在控制台绘图原理: 通过Graphics进行绘图获取控制台的窗口句柄[DllImport(&quot;kernel32.dll&quot;)]static extern IntPtr GetConsoleWindow();获取Graphics对象Graphics g = Graphics.FromHwnd(GetConsoleWindow());于是乎, 你就可以通过获取的Graphics对象随便进行绘图了!但是, 注意, 当控制条刷新的时候, 比如Console.Clear(), 或者控制</description><pubDate>Sun, 15 Nov 2020 21:59:01 GMT</pubDate><content:encoded>&lt;h1&gt;[C#] 在控制台绘图&lt;/h1&gt;
&lt;h4&gt;原理: 通过Graphics进行绘图&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;获取控制台的窗口句柄&lt;pre&gt;&lt;code&gt;[DllImport(&quot;kernel32.dll&quot;)]
static extern IntPtr GetConsoleWindow();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;获取Graphics对象&lt;pre&gt;&lt;code&gt;Graphics g = Graphics.FromHwnd(GetConsoleWindow());
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;于是乎, 你就可以通过获取的Graphics对象随便进行绘图了!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;但是, 注意, 当控制条刷新的时候, 比如Console.Clear(), 或者控制台光标经过绘图区域, 绘制的内容就会失效, 这时你需要重新绘制. (如果有控制台刷新的事件就好了)&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>[C#] 运算包含数学表达式的字符串</title><link>https://slimenull.com/posts/20201029114122/</link><guid isPermaLink="true">https://slimenull.com/posts/20201029114122/</guid><description>关于:原理讲解代码示例完整程序源码下载适用于:实例代码适用于: .NET Framework &amp; .NET Core算法通用. 只要你能够找到与算法对应的实现方式.将要实现:分析表达式实现所有的通用运算符实现三元运算符原理:使用&quot;状态机&quot;算法分析表达式根据运算符优先级, 不断尝试运算, 最终得到结果详解:第一步, 我们需要将运算表达式分成一个个节点(token), 这个节点可能是一个数字, 可能是一个运算符, 至于表达式里的括号, 我们会使用递归来</description><pubDate>Thu, 29 Oct 2020 11:41:22 GMT</pubDate><content:encoded>&lt;h2&gt;关于:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;h6&gt;原理讲解&lt;/h6&gt;
&lt;/li&gt;
&lt;li&gt;代码示例&lt;/li&gt;
&lt;li&gt;完整程序源码下载&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;适用于:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;实例代码适用于: .NET Framework &amp;amp; .NET Core&lt;/li&gt;
&lt;li&gt;算法通用. 只要你能够找到与算法对应的实现方式.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;将要实现:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;分析表达式&lt;/li&gt;
&lt;li&gt;实现所有的通用运算符&lt;/li&gt;
&lt;li&gt;实现三元运算符&lt;/li&gt;
&lt;li&gt;支持嵌套的括号&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;原理:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;使用&quot;状态机&quot;算法分析表达式&lt;/li&gt;
&lt;li&gt;根据运算符优先级, 不断尝试运算, 最终得到结果&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;详解:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;第一步, 我们需要将运算表达式分成一个个节点(token), 这个节点可能是一个数字, 可能是一个运算符, 至于表达式里的括号, 我们会使用递归来进行运算. 我们使用下面的class来表示这个token.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;enum CalcTokenType
{
    // 规定枚举所对应的int为2的倍数是为了后面方便识别
    None = 0, Number = 1, Operator = 2
}

enum CalcOperator
{
    None,
    Add = 1,       // +
    Sub = 2,       // -
    Mul = 4,       // *
    Div = 8,       // /
    Mod = 16,      // %
    Pow = 32,      // **
    Lss = 64,      // &amp;lt;
    Gtr = 128,     // &amp;gt;
    Leq = 256,     // &amp;lt;=
    Geq = 512,     // &amp;gt;=
    Equ = 1024,    // =
    Neq = 2048,    // !=
    Not = 4096,    // !
    And = 8192,    // &amp;amp;
    Or = 16384,    // |
    If = 32768,    // 三元表达式中的 &apos;?&apos; 符号
    Witch = 65536  // 三元表达式中的 &apos;:&apos; 符号
}

class CalcToken
{
    public CalcToken(CalcTokenType type, object content)
    {
        this.Type = type;
        this.Content = content;
    }
    CalcTokenType Type;
    object Content;
    // Content, 当Type是None时, 值应该是null
    // 当Type是Number时, 值应该是double
    // 当Type是Operator时, 值应该是CalcOperator枚举变量
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;继续, 那么, 先是如何分析一个简单的表达式, 例如下面的:&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;11+1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;分析这个表达式, 我们首先定义一些字符串常量, 如下:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;char[] OperatorChars = &quot;+-*/&amp;lt;&amp;gt;!=&amp;amp;|?:&quot;.ToArray();  // 表示操作符的字符
char[] NumberChars = &quot;0123456789.&quot;.ToArray();     // 表示数字的字符
char[] EmptyChars = &quot; \t&quot;.ToArray();          // 表示空字符
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;我们将逐字符对其分析(有限状态机): 使用List&amp;lt;CalcToken&amp;gt; tokens来保存分析出的token&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;当开始时, 我们取到一个字符&apos;1&apos;, 现在我们还不是在分析任何类型, 但是经过定义的字符集常量的Contains判断, 我们发现它是属于数字的, 那么我们知道, 现在我们正在分析数字.&lt;/li&gt;
&lt;li&gt;存储下来这个字符&apos;1&apos;, 用什么, 你应该知道吧? 要么是List&amp;lt;char&amp;gt;, 要么是StringBuilder. 并且, 记录下我们现在的状态, 我们分析到了数字&lt;/li&gt;
&lt;li&gt;到分析到第二个数字时, 我们当前的状态是正在分析数字, 那么在这个状态下, 如果我们又取到一个数字, 那么我们需要存储下这个数字, 并且当前状态不变, 如果我们取到一个操作符字符, 那么代表现在这个数字结束了, 使用double.Parse将已保存的字符串(一个或多个由数字字符组成的), 转换为double类型, 然后存储下这个token, 并且清空存储的字符, 将当前取到的字符存进去, 然后改变当前状态到&quot;正在分析操作符&quot;.&lt;/li&gt;
&lt;li&gt;我们的表达式是&quot;11+1&quot;, 第二个字符是一个数字字符, 也就是说, 我们应存下这个字符, 且状态不变. 于是, 我们已经存储下了两个字符{&apos;1&apos;, &apos;1&apos;}, 当前状态是数字&lt;/li&gt;
&lt;li&gt;到第三个字符了, 我们发现它不是一个数字字符, 而是一个操作符字符, 于是, 将已保存的字符串转换为double, 并添加到tokens, 当前状态是:正在分析操作符&lt;/li&gt;
&lt;li&gt;到第四个字符, 它又不是一个操作符字符了, 它现在是一个数字, 所以, 将已存储的操作符字符保存&lt;/li&gt;
&lt;li&gt;到最后, 分析结束, 但临时存储的部分还有一个字符&apos;1&apos;, 将其转换为double, 然后存储到tokens, 于是, 我们成功完成了对表达式的分词, 至此, 基本原理已经讲清, 尝试理解下面的函数&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// 下面是分词核心, 这里面包含对括号的分析
// bool inner表示这个函数是否是被另一个分词函数调用的, 也就是说是否在递归状态, 如果是, 则表示在分析一个括号内的表达式, 碰到&apos;)&apos;的时候立即return;
bool TryParseCalcTokens(ref char[] source, ref int offset, out List&amp;lt;CalcToken&amp;gt; result, bool inner = false, int level = 0)
{
    result = new List&amp;lt;CalcToken&amp;gt;();

    for (; offset &amp;lt; source.Length; offset++)
    {
        if (EmptyChars.Contains(source[offset]))
        {
            continue;
        }
        else if (source[offset] == &apos;(&apos;)
        {
            offset++;
            if (TryParseCalcTokens(ref source, ref offset, out List&amp;lt;CalcToken&amp;gt; newrst, true, level + 1))
            {
                result.Add(new CalcToken(CalcTokenType.Expression, newrst));
            }
            else
            {
                errors.Add($&quot;Error when parsing inner expression. Level:{level};&quot;);
                return false;
            }
        }
        else if (source[offset] == &apos;)&apos;)
        {
            return true;
        }
        else
        {
            if (TryParseToken(ref source, ref offset, out CalcToken newcctk))
            {
                result.Add(newcctk);   // 这里是将分析好的token添加到tokens
            }
            else
            {
                errors.Add($&quot;Error when parsing expression token. Level:{level};&quot;);
                return false;
            }
        }
    }


    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;开始运算表达式: (最简单的方法)&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;遍历我们的tokens, 第一次遍历, 查找优先级最高的操作符并运算结果, 第二次遍历查找优先级略次的操作符并运算结果, 如此往复, 查找完所有的表达式&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// 下面是运算核心
bool TryCalculateTokens(ref List&amp;lt;CalcToken&amp;gt; source, out double result, out List&amp;lt;string&amp;gt; errors)
{
    errors = new List&amp;lt;string&amp;gt;();
    result = 0;

    for (int i = 0; i &amp;lt; source.Count; i++)
    {
        if (source[i].Type == CalcTokenType.Operator &amp;amp;&amp;amp; CheckOperator(CalcOperator.Mul | CalcOperator.Div | CalcOperator.Mod | CalcOperator.Pow, (CalcOperator)source[i].Content) &amp;amp;&amp;amp; CheckType(ref source, CalcTokenType.Number | CalcTokenType.Expression, i, -1) &amp;amp;&amp;amp; CheckType(ref source, CalcTokenType.Number | CalcTokenType.Expression, i, 1))
        {
            if (TryGetTokenValue(ref errors, source[i - 1], out double leftnum) &amp;amp;&amp;amp; TryGetTokenValue(ref errors, source[i + 1], out double rightnum))
            {
                double tmpnum = 0;
                switch ((CalcOperator)source[i].Content)
                {
                    case CalcOperator.Mul:
                        tmpnum = leftnum * rightnum;
                        break;
                    case CalcOperator.Div:
                        tmpnum = leftnum / rightnum;
                        break;
                    case CalcOperator.Mod:
                        tmpnum = leftnum % rightnum;
                        break;
                    case CalcOperator.Pow:
                        tmpnum = Math.Pow(leftnum, rightnum);
                        break;
                    default:
                        throw new Exception();
                }
                source[i - 1] = new CalcToken(CalcTokenType.Number, tmpnum);
                source.RemoveRange(i, 2);
                i--;
            }
            else
            {
                errors.Add($&quot;Cannot get the value of token when calculate &apos;{source[i].Content}&apos;;&quot;);
                return false;
            }
        }
    }    // * / % **
    for (int i = 0; i &amp;lt; source.Count; i++)
    {
        if (source[i].Type == CalcTokenType.Operator &amp;amp;&amp;amp; CheckOperator(CalcOperator.Add | CalcOperator.Sub, (CalcOperator)source[i].Content) &amp;amp;&amp;amp; CheckType(ref source, CalcTokenType.Number | CalcTokenType.Expression, i, 1))
        {
            if (CheckType(ref source, CalcTokenType.Number | CalcTokenType.Expression, i, -1))
            {
                if (TryGetTokenValue(ref errors, source[i - 1], out double leftnum) &amp;amp;&amp;amp; TryGetTokenValue(ref errors, source[i + 1], out double rightnum))
                {
                    double tmpnum = 0;
                    switch ((CalcOperator)source[i].Content)
                    {
                        case CalcOperator.Add:
                            tmpnum = leftnum + rightnum;
                            break;
                        case CalcOperator.Sub:
                            tmpnum = leftnum - rightnum;
                            break;
                        default:
                            throw new Exception();
                    }
                    source[i - 1] = new CalcToken(CalcTokenType.Number, tmpnum);
                    source.RemoveRange(i, 2);
                    i--;
                }
                else
                {
                    errors.Add($&quot;Cannot get the value of token when calculate &apos;{source[i].Content}&apos;;&quot;);
                    return false;
                }
            }
            else
            {
                if (TryGetTokenValue(ref errors, source[i + 1], out double rightnum))
                {
                    double tmpnum = 0;
                    switch ((CalcOperator)source[i].Content)
                    {
                        case CalcOperator.Add:
                            tmpnum = rightnum;
                            break;
                        case CalcOperator.Sub:
                            tmpnum = 0 - rightnum;
                            break;
                        default:
                            throw new Exception();
                    }
                    source[i] = new CalcToken(CalcTokenType.Number, tmpnum);
                    source.RemoveRange(i + 1, 1);
                }
                else
                {
                    errors.Add($&quot;Cannot get the value of token when calculate &apos;{source[i].Content}&apos;;&quot;);
                    return false;
                }
            }
        }
    }    // + -
    for (int i = 0; i &amp;lt; source.Count; i++)
    {
        if (source[i].Type == CalcTokenType.Operator &amp;amp;&amp;amp; CheckOperator(CalcOperator.Gtr | CalcOperator.Lss | CalcOperator.Geq | CalcOperator.Leq, (CalcOperator)source[i].Content) &amp;amp;&amp;amp; CheckType(ref source, CalcTokenType.Number | CalcTokenType.Expression, i, -1) &amp;amp;&amp;amp; CheckType(ref source, CalcTokenType.Number | CalcTokenType.Expression, i, 1))
        {
            if (TryGetTokenValue(ref errors, source[i - 1], out double leftnum) &amp;amp;&amp;amp; TryGetTokenValue(ref errors, source[i + 1], out double rightnum))
            {
                double tmpnum = 0;
                switch ((CalcOperator)source[i].Content)
                {
                    case CalcOperator.Gtr:
                        tmpnum = leftnum &amp;gt; rightnum ? 1 : 0;
                        break;
                    case CalcOperator.Lss:
                        tmpnum = leftnum &amp;lt; rightnum ? 1 : 0;
                        break;
                    case CalcOperator.Geq:
                        tmpnum = leftnum &amp;gt;= rightnum ? 1 : 0;
                        break;
                    case CalcOperator.Leq:
                        tmpnum = leftnum &amp;lt;= rightnum ? 1 : 0;
                        break;
                    default:
                        throw new Exception();
                }
                source[i - 1] = new CalcToken(CalcTokenType.Number, tmpnum);
                source.RemoveRange(i, 2);
                i--;
            }
            else
            {
                errors.Add($&quot;Cannot get the value of token when calculate &apos;{source[i].Content}&apos;;&quot;);
                return false;
            }
        }
    }    // &amp;gt; &amp;lt; &amp;gt;= &amp;lt;=
    for (int i = 0; i &amp;lt; source.Count; i++)
    {
        if (source[i].Type == CalcTokenType.Operator &amp;amp;&amp;amp; CheckOperator(CalcOperator.Equ | CalcOperator.Neq, (CalcOperator)source[i].Content) &amp;amp;&amp;amp; CheckType(ref source, CalcTokenType.Number | CalcTokenType.Expression, i, -1) &amp;amp;&amp;amp; CheckType(ref source, CalcTokenType.Number | CalcTokenType.Expression, i, 1))
        {
            if (TryGetTokenValue(ref errors, source[i - 1], out double leftnum) &amp;amp;&amp;amp; TryGetTokenValue(ref errors, source[i + 1], out double rightnum))
            {
                double tmpnum = 0;
                switch ((CalcOperator)source[i].Content)
                {
                    case CalcOperator.Equ:
                        tmpnum = leftnum == rightnum ? 1 : 0;
                        break;
                    case CalcOperator.Neq:
                        tmpnum = leftnum != rightnum ? 1 : 0;
                        break;
                    default:
                        throw new Exception();
                }
                source[i - 1] = new CalcToken(CalcTokenType.Number, tmpnum);
                source.RemoveRange(i, 2);
                i--;
            }
            else
            {
                errors.Add($&quot;Cannot get the value of token when calculate &apos;{source[i].Content}&apos;;&quot;);
                return false;
            }
        }
    }    // == !=
    for (int i = 0; i &amp;lt; source.Count; i++)
    {
        if (source[i].Type == CalcTokenType.Operator &amp;amp;&amp;amp; CheckOperator(CalcOperator.Add | CalcOperator.Or, (CalcOperator)source[i].Content) &amp;amp;&amp;amp; CheckType(ref source, CalcTokenType.Number | CalcTokenType.Expression, i, -1) &amp;amp;&amp;amp; CheckType(ref source, CalcTokenType.Number | CalcTokenType.Expression, i, 1))
        {
            if (TryGetTokenValue(ref errors, source[i - 1], out double leftnum) &amp;amp;&amp;amp; TryGetTokenValue(ref errors, source[i + 1], out double rightnum))
            {
                double tmpnum = 0;
                switch ((CalcOperator)source[i].Content)
                {
                    case CalcOperator.Add:
                        tmpnum = leftnum != 0 &amp;amp;&amp;amp; rightnum != 0 ? 1 : 0;
                        break;
                    case CalcOperator.Or:
                        tmpnum = leftnum != 0 || rightnum != 0 ? 1 : 0;
                        break;
                    default:
                        throw new Exception();
                }
                source[i - 1] = new CalcToken(CalcTokenType.Number, tmpnum);
                source.RemoveRange(i, 2);
                i--;
            }
            else
            {
                errors.Add($&quot;Cannot get the value of token when calculate &apos;{source[i].Content}&apos;;&quot;);
                return false;
            }
        }
    }    // &amp;amp; |
    for (int i = 0; i &amp;lt; source.Count; i++)
    {
        if (source[i].Type == CalcTokenType.Operator &amp;amp;&amp;amp; CheckOperator(CalcOperator.If, (CalcOperator)source[i].Content) &amp;amp;&amp;amp; CheckType(ref source, CalcTokenType.Number | CalcTokenType.Expression, i, -1) &amp;amp;&amp;amp; CheckType(ref source, CalcTokenType.Number | CalcTokenType.Expression, i, 1) &amp;amp;&amp;amp; CheckType(ref source, CalcTokenType.Operator, i, 2) &amp;amp;&amp;amp; CheckType(ref source, CalcTokenType.Number | CalcTokenType.Expression, i, 3) &amp;amp;&amp;amp; CheckOperator(CalcOperator.Witch, (CalcOperator)source[i + 2].Content))
        {
            if (TryGetTokenValue(ref errors, source[i - 1], out double basic))
            {
                double tmpnum = 0;
                if (basic != 0)
                {
                    if (TryGetTokenValue(ref errors, source[i + 1], out double leftnum))
                    {
                        tmpnum = leftnum;
                    }
                    else
                    {
                        errors.Add($&quot;Cannot get the value of token when calculate &apos;{source[i].Content}&apos;;&quot;);
                        return false;
                    }
                }
                else
                {
                    if (TryGetTokenValue(ref errors, source[i + 3], out double rightnum))
                    {
                        tmpnum = rightnum;
                    }
                    else
                    {
                        errors.Add($&quot;Cannot get the value of token when calculate &apos;{source[i].Content}&apos;;&quot;);
                        return false;
                    }
                }
                source[i - 1] = new CalcToken(CalcTokenType.Number, tmpnum);
                source.RemoveRange(i, 4);
                i--;
            }
            else
            {
                errors.Add($&quot;Cannot get the value of token when calculate &apos;{source[i].Content}&apos;;&quot;);
                return false;
            }
        }
    }    // 三元表达式

    if (source.Count == 1 &amp;amp;&amp;amp; source[0].Type == CalcTokenType.Number)
    {
        result = (double)source[0].Content;
        return true;
    }
    else
    {
        errors.Add($&quot;Final result after calculate is not a single number. &apos;{string.Join(&quot; &quot;, source)}&apos;;&quot;);
        return false;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&amp;lt;br /&amp;gt;&amp;lt;br /&amp;gt;&amp;lt;br /&amp;gt;&amp;lt;br /&amp;gt;&amp;lt;br /&amp;gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;完整源代码:&amp;lt;a href=&quot;https://github.com/SlimeNull/CalculatorPlus&quot;&amp;gt;Github仓库&amp;lt;/a&amp;gt;
最新版本已经使用了逆波兰表达式&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[C#] CHO.Json操作Json数据</title><link>https://slimenull.com/posts/20201028235244/</link><guid isPermaLink="true">https://slimenull.com/posts/20201028235244/</guid><description>这是一个类似于Newtonsoft.Json的项目, 但与其有些出入。这是它与Newtonsoft.Json的差别:CHO.Json支持你像Python那样不需要实体类而简便的操作小型数据, 也支持将类的实例序列化为Json文本与将分析完毕的Json数据反序列化为特定类的实例CHO.Json少了许多冗余的功能, 例如将图片序列化为字符串, 因此CHO.Json可能要比Newtonsoft.Json轻量许多。CHO.Json的源代码比Newtonsoft.Json更适合初学者阅读, 在看懂它的代.</description><pubDate>Wed, 28 Oct 2020 23:52:44 GMT</pubDate><content:encoded>&lt;h2&gt;CHO.Json：&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;现在，CHO.Json已经成为了一个强大的Json解析库，速度很快。&lt;/li&gt;
&lt;li&gt;支持弱类型操作, JsonData实现了所有Json数据类型的基本操作, 支持像 Python 那样的操作方式&lt;/li&gt;
&lt;li&gt;支持强类型操作, JsonData可以直接通过 ‘Get类型’ 方法来获取对应数据类型数据并进行操作&lt;/li&gt;
&lt;li&gt;支持序列化， 反序列化， 一次分析多个Json数据：
&lt;ol&gt;
&lt;li&gt;序列化和反序列化则是所有库都有的功能&lt;/li&gt;
&lt;li&gt;一次性序列化多个Json数据则是，允许一段文本中含有多个Json根数据，它可以用于解决TCP套接字的粘包问题&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;CHO.Json 拥有教快的分析速度，能够完全与Json.Net与Text.Json匹敌，并且它的加载速度时三者中最快的。
&lt;img src=&quot;/images/20201231000802877.png&quot; alt=&quot;实验数据：CHO.Json的速度优势&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;这是它与Newtonsoft.Json的差别:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CHO.Json少了许多冗余的功能, 例如将图片序列化为字符串, 因此CHO.Json可能要比Newtonsoft.Json轻量许多。&lt;/li&gt;
&lt;li&gt;CHO.Json的源代码比Newtonsoft.Json更适合初学者阅读, 在看懂它的代码后, 你会了解到有限状态机以及反射&lt;/li&gt;
&lt;li&gt;CHO.Json仅使用一个C#源文件, 这是因为它的源代码仅有1.5k行左右, 这其中还包含类型转换等片段&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;更详细的介绍：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先， 在 CHO.Json 中, 包含一个 JsonData 类, 它可以用来表示任何 Json 数据, 通过 JsonData 类的静态方法, 可以从字符串或字符数组中分析出 Json 数据并存储到 JsonData 实例中, 或者将 JsonData 实例转化为 Json 字符串.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JsonData 是你最常用的类, 有一个 DataType 属性指定了这个实例所表示的 Json 数据类型, 例如String, Boolean, Integer, 通过 &apos;Get类型&apos; 方法可以直接获取对应数据, 例如: GetString() 方法返回这个Json实例中所包含的字符串信息. 但如果你对一个包含了非字符串信息的 JsonData 实例使用这个方法, 则会抛出异常.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在最新版本的 CHO.Json 中, Serializie(序列化)和Deserialize(反序列化)用于直接转换字符串和指定的数据. 但在旧版, 则是转换 JsonData. 新版中, 推荐的方法是使用 JsonData 的静态方法: Parse, Create, ConvertToInstance, ConvertToText.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果一个字符串里包含多个Json数据, 但并没有分隔符, 例如在TCP套接字中传输的多个Json文本, 你可以通过JsonData的静态方法 ParseStream 来分析它们.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JsonData 的 Content 属性是 JsonData 包含的数据原型, 如果是Array, 则它的类型是List&amp;lt;JsonData&amp;gt;, 如果是Object, 则它的类型是 Dictionary&amp;lt;JsonData, JsonData&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20201231002914420.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;下面是使用CHO.Json的例子:&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/20201231002450466.png&quot; alt=&quot;使用套接字发送经过CHO.Json转换过的信息&quot; /&gt;&lt;/p&gt;
&lt;p&gt;项目完整源代码: &lt;a href=&quot;https://github.com/SlimeNull/EleCho.Json&quot;&gt;Github仓库&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>[C#] 计算字符串在控制台上显示的宽度, 包含所有Char能表示的字符!</title><link>https://slimenull.com/posts/20201028232711/</link><guid isPermaLink="true">https://slimenull.com/posts/20201028232711/</guid><description>通过运算, 获取字符串在控制台上现实的宽度(单位为一个英文字母的宽度, 高度为控制台中一行的高度)在网上找了半天, 唯一一个正儿八经的, 就是通过GBK编码的字节数来推测所占宽度, 但我认为这个有点不大妥当, 例如某些特殊字符, 可能就不被GBK编码包含.所以, 我这里提供了一个可获取字符串显示宽度的可行方法.首先, 我通过循环C#中所有的字符, 并将其打印到控制台上, 运算单个字符所占宽度, 最终得出了一个List&lt;int&gt;, 通过这个列表, 只需要以字符强制转换为int的.</description><pubDate>Wed, 28 Oct 2020 23:27:11 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;通过运算, 获取字符串在控制台上现实的宽度(单位为一个英文字母的宽度, 高度为控制台中一行的高度)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;在网上找了半天, 唯一一个正儿八经的, 就是通过GBK编码的字节数来推测所占宽度, 但我认为这个有点不大妥当, 例如某些特殊字符, 可能就不被GBK编码包含.&lt;/li&gt;
&lt;li&gt;所以, 我这里提供了一个可获取字符串显示宽度的可行方法.
&lt;strong&gt;Github 仓库地址: &lt;a href=&quot;https://github.com/SlimeNull/Null.ConsoleEx&quot;&gt;Null.ConsoleEx&lt;/a&gt;&lt;/strong&gt; ==重点==&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;实现过程:&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;首先, 我通过循环C#中所有的字符, 并将其打印到控制台上, 运算单个字符所占宽度, 最终得出了一个List&amp;lt;int&amp;gt;, 通过这个列表, 只需要以字符强制转换为int的值作为索引, 访问对应的元素, 即可得出字符对应宽度&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int GetCharLen(char c)
{
    Console.CursorLeft = Console.BufferWidth / 2 - 1;
    int start = Console.CursorLeft;
    Console.Write(c);
    return Console.CursorLeft - start;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;之后, 再获取区间, 稍加处理, 即得到可获取char所有字符在控制台中显示的宽度.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public static int GetCharDisplayLength(char c)
{
    if (index &amp;lt; 7)
        return 1;
    else if (index &amp;lt; 9)
        return 0;
    else if (index &amp;lt; 10)
        return 8;
    else if (index &amp;lt; 11)
        return 0;
    else if (index &amp;lt; 13)
        return 1;
    else if (index &amp;lt; 14)
        return 0;
    else if (index &amp;lt; 162)
        return 1;
    else if (index &amp;lt; 166)
        return 2;
    else if (index &amp;lt; 167)
        return 1;
    else if (index &amp;lt; 169)
        return 2;
    else if (index &amp;lt; 175)
        return 1;
    else if (index &amp;lt; 178)
        return 2;
    else if (index &amp;lt; 180)
        return 1;
    else if (index &amp;lt; 182)
        return 2;
    else if (index &amp;lt; 183)
        return 1;
    else if (index &amp;lt; 184)
        return 2;
    else if (index &amp;lt; 215)
        return 1;
    else if (index &amp;lt; 216)
        return 2;
    else if (index &amp;lt; 247)
        return 1;
    else if (index &amp;lt; 248)
        return 2;
    else if (index &amp;lt; 449)
        return 1;
    else if (index &amp;lt; 450)
        return 2;
    else if (index &amp;lt; 711)
        return 1;
    else if (index &amp;lt; 712)
        return 2;
    else if (index &amp;lt; 713)
        return 1;
    else if (index &amp;lt; 716)
        return 2;
    else if (index &amp;lt; 729)
        return 1;
    else if (index &amp;lt; 730)
        return 2;
    else if (index &amp;lt; 913)
        return 1;
    else if (index &amp;lt; 930)
        return 2;
    else if (index &amp;lt; 931)
        return 1;
    else if (index &amp;lt; 938)
        return 2;
    else if (index &amp;lt; 945)
        return 1;
    else if (index &amp;lt; 962)
        return 2;
    else if (index &amp;lt; 963)
        return 1;
    else if (index &amp;lt; 970)
        return 2;
    else if (index &amp;lt; 1025)
        return 1;
    else if (index &amp;lt; 1026)
        return 2;
    else if (index &amp;lt; 1040)
        return 1;
    else if (index &amp;lt; 1104)
        return 2;
    else if (index &amp;lt; 1105)
        return 1;
    else if (index &amp;lt; 1106)
        return 2;
    else if (index &amp;lt; 8208)
        return 1;
    else if (index &amp;lt; 8209)
        return 2;
    else if (index &amp;lt; 8211)
        return 1;
    else if (index &amp;lt; 8215)
        return 2;
    else if (index &amp;lt; 8216)
        return 1;
    else if (index &amp;lt; 8218)
        return 2;
    else if (index &amp;lt; 8220)
        return 1;
    else if (index &amp;lt; 8222)
        return 2;
    else if (index &amp;lt; 8229)
        return 1;
    else if (index &amp;lt; 8231)
        return 2;
    else if (index &amp;lt; 8240)
        return 1;
    else if (index &amp;lt; 8241)
        return 2;
    else if (index &amp;lt; 8242)
        return 1;
    else if (index &amp;lt; 8244)
        return 2;
    else if (index &amp;lt; 8245)
        return 1;
    else if (index &amp;lt; 8246)
        return 2;
    else if (index &amp;lt; 8251)
        return 1;
    else if (index &amp;lt; 8252)
        return 2;
    else if (index &amp;lt; 8254)
        return 1;
    else if (index &amp;lt; 8255)
        return 2;
    else if (index &amp;lt; 8364)
        return 1;
    else if (index &amp;lt; 8365)
        return 2;
    else if (index &amp;lt; 8451)
        return 1;
    else if (index &amp;lt; 8452)
        return 2;
    else if (index &amp;lt; 8453)
        return 1;
    else if (index &amp;lt; 8454)
        return 2;
    else if (index &amp;lt; 8457)
        return 1;
    else if (index &amp;lt; 8458)
        return 2;
    else if (index &amp;lt; 8470)
        return 1;
    else if (index &amp;lt; 8471)
        return 2;
    else if (index &amp;lt; 8481)
        return 1;
    else if (index &amp;lt; 8482)
        return 2;
    else if (index &amp;lt; 8544)
        return 1;
    else if (index &amp;lt; 8556)
        return 2;
    else if (index &amp;lt; 8560)
        return 1;
    else if (index &amp;lt; 8570)
        return 2;
    else if (index &amp;lt; 8592)
        return 1;
    else if (index &amp;lt; 8596)
        return 2;
    else if (index &amp;lt; 8598)
        return 1;
    else if (index &amp;lt; 8602)
        return 2;
    else if (index &amp;lt; 8712)
        return 1;
    else if (index &amp;lt; 8713)
        return 2;
    else if (index &amp;lt; 8719)
        return 1;
    else if (index &amp;lt; 8720)
        return 2;
    else if (index &amp;lt; 8721)
        return 1;
    else if (index &amp;lt; 8722)
        return 2;
    else if (index &amp;lt; 8725)
        return 1;
    else if (index &amp;lt; 8726)
        return 2;
    else if (index &amp;lt; 8728)
        return 1;
    else if (index &amp;lt; 8729)
        return 2;
    else if (index &amp;lt; 8730)
        return 1;
    else if (index &amp;lt; 8731)
        return 2;
    else if (index &amp;lt; 8733)
        return 1;
    else if (index &amp;lt; 8737)
        return 2;
    else if (index &amp;lt; 8739)
        return 1;
    else if (index &amp;lt; 8740)
        return 2;
    else if (index &amp;lt; 8741)
        return 1;
    else if (index &amp;lt; 8742)
        return 2;
    else if (index &amp;lt; 8743)
        return 1;
    else if (index &amp;lt; 8748)
        return 2;
    else if (index &amp;lt; 8750)
        return 1;
    else if (index &amp;lt; 8751)
        return 2;
    else if (index &amp;lt; 8756)
        return 1;
    else if (index &amp;lt; 8760)
        return 2;
    else if (index &amp;lt; 8764)
        return 1;
    else if (index &amp;lt; 8766)
        return 2;
    else if (index &amp;lt; 8776)
        return 1;
    else if (index &amp;lt; 8777)
        return 2;
    else if (index &amp;lt; 8780)
        return 1;
    else if (index &amp;lt; 8781)
        return 2;
    else if (index &amp;lt; 8786)
        return 1;
    else if (index &amp;lt; 8787)
        return 2;
    else if (index &amp;lt; 8800)
        return 1;
    else if (index &amp;lt; 8802)
        return 2;
    else if (index &amp;lt; 8804)
        return 1;
    else if (index &amp;lt; 8808)
        return 2;
    else if (index &amp;lt; 8814)
        return 1;
    else if (index &amp;lt; 8816)
        return 2;
    else if (index &amp;lt; 8853)
        return 1;
    else if (index &amp;lt; 8854)
        return 2;
    else if (index &amp;lt; 8857)
        return 1;
    else if (index &amp;lt; 8858)
        return 2;
    else if (index &amp;lt; 8869)
        return 1;
    else if (index &amp;lt; 8870)
        return 2;
    else if (index &amp;lt; 8895)
        return 1;
    else if (index &amp;lt; 8896)
        return 2;
    else if (index &amp;lt; 8978)
        return 1;
    else if (index &amp;lt; 8979)
        return 2;
    else if (index &amp;lt; 9312)
        return 1;
    else if (index &amp;lt; 9322)
        return 2;
    else if (index &amp;lt; 9332)
        return 1;
    else if (index &amp;lt; 9372)
        return 2;
    else if (index &amp;lt; 9632)
        return 1;
    else if (index &amp;lt; 9634)
        return 2;
    else if (index &amp;lt; 9650)
        return 1;
    else if (index &amp;lt; 9652)
        return 2;
    else if (index &amp;lt; 9660)
        return 1;
    else if (index &amp;lt; 9662)
        return 2;
    else if (index &amp;lt; 9670)
        return 1;
    else if (index &amp;lt; 9672)
        return 2;
    else if (index &amp;lt; 9675)
        return 1;
    else if (index &amp;lt; 9676)
        return 2;
    else if (index &amp;lt; 9678)
        return 1;
    else if (index &amp;lt; 9680)
        return 2;
    else if (index &amp;lt; 9698)
        return 1;
    else if (index &amp;lt; 9702)
        return 2;
    else if (index &amp;lt; 9733)
        return 1;
    else if (index &amp;lt; 9735)
        return 2;
    else if (index &amp;lt; 9737)
        return 1;
    else if (index &amp;lt; 9738)
        return 2;
    else if (index &amp;lt; 9792)
        return 1;
    else if (index &amp;lt; 9793)
        return 2;
    else if (index &amp;lt; 9794)
        return 1;
    else if (index &amp;lt; 9795)
        return 2;
    else if (index &amp;lt; 12288)
        return 1;
    else if (index &amp;lt; 12292)
        return 2;
    else if (index &amp;lt; 12293)
        return 1;
    else if (index &amp;lt; 12312)
        return 2;
    else if (index &amp;lt; 12317)
        return 1;
    else if (index &amp;lt; 12319)
        return 2;
    else if (index &amp;lt; 12321)
        return 1;
    else if (index &amp;lt; 12330)
        return 2;
    else if (index &amp;lt; 12353)
        return 1;
    else if (index &amp;lt; 12436)
        return 2;
    else if (index &amp;lt; 12443)
        return 1;
    else if (index &amp;lt; 12447)
        return 2;
    else if (index &amp;lt; 12449)
        return 1;
    else if (index &amp;lt; 12535)
        return 2;
    else if (index &amp;lt; 12540)
        return 1;
    else if (index &amp;lt; 12543)
        return 2;
    else if (index &amp;lt; 12549)
        return 1;
    else if (index &amp;lt; 12586)
        return 2;
    else if (index &amp;lt; 12690)
        return 1;
    else if (index &amp;lt; 12704)
        return 2;
    else if (index &amp;lt; 12832)
        return 1;
    else if (index &amp;lt; 12868)
        return 2;
    else if (index &amp;lt; 12928)
        return 1;
    else if (index &amp;lt; 12958)
        return 2;
    else if (index &amp;lt; 12959)
        return 1;
    else if (index &amp;lt; 12964)
        return 2;
    else if (index &amp;lt; 12969)
        return 1;
    else if (index &amp;lt; 12977)
        return 2;
    else if (index &amp;lt; 13198)
        return 1;
    else if (index &amp;lt; 13200)
        return 2;
    else if (index &amp;lt; 13212)
        return 1;
    else if (index &amp;lt; 13215)
        return 2;
    else if (index &amp;lt; 13217)
        return 1;
    else if (index &amp;lt; 13218)
        return 2;
    else if (index &amp;lt; 13252)
        return 1;
    else if (index &amp;lt; 13253)
        return 2;
    else if (index &amp;lt; 13262)
        return 1;
    else if (index &amp;lt; 13263)
        return 2;
    else if (index &amp;lt; 13265)
        return 1;
    else if (index &amp;lt; 13267)
        return 2;
    else if (index &amp;lt; 13269)
        return 1;
    else if (index &amp;lt; 13270)
        return 2;
    else if (index &amp;lt; 19968)
        return 1;
    else if (index &amp;lt; 40870)
        return 2;
    else if (index &amp;lt; 55296)
        return 1;
    else if (index &amp;lt; 55297)
        return 0;
    else if (index &amp;lt; 56320)
        return 1;
    else if (index &amp;lt; 56321)
        return 2;
    else if (index &amp;lt; 57344)
        return 1;
    else if (index &amp;lt; 59335)
        return 2;
    else if (index &amp;lt; 59337)
        return 1;
    else if (index &amp;lt; 59493)
        return 2;
    else if (index &amp;lt; 63733)
        return 1;
    else if (index &amp;lt; 63734)
        return 2;
    else if (index &amp;lt; 63744)
        return 1;
    else if (index &amp;lt; 64046)
        return 2;
    else if (index &amp;lt; 65072)
        return 1;
    else if (index &amp;lt; 65074)
        return 2;
    else if (index &amp;lt; 65075)
        return 1;
    else if (index &amp;lt; 65093)
        return 2;
    else if (index &amp;lt; 65097)
        return 1;
    else if (index &amp;lt; 65107)
        return 2;
    else if (index &amp;lt; 65108)
        return 1;
    else if (index &amp;lt; 65112)
        return 2;
    else if (index &amp;lt; 65113)
        return 1;
    else if (index &amp;lt; 65127)
        return 2;
    else if (index &amp;lt; 65128)
        return 1;
    else if (index &amp;lt; 65132)
        return 2;
    else if (index &amp;lt; 65281)
        return 1;
    else if (index &amp;lt; 65375)
        return 2;
    else if (index &amp;lt; 65504)
        return 1;
    else if (index &amp;lt; 65510)
        return 2;
    else if (index &amp;lt; 65536)
        return 1;
    else
        return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再稍加处理, 使得每一个字符所需进行的判断次数差异减少. (类似于二叉树),
&lt;strong&gt;下面这个代码直接就能用哦~ 而且是包含Char能表示的所有字符的.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static int GetCharDisplayLength(char c)
{
    int index = c;
    if (index &amp;lt; 8800)
    {
        if (index &amp;lt; 8246)
        {
            if (index &amp;lt; 913)
            {
                if (index &amp;lt; 183)
                {
                    if (index &amp;lt; 166)
                    {
                        if (index &amp;lt; 11)
                        {
                            if (index &amp;lt; 9)
                            {
                                if (index &amp;lt; 7)
                                    return 1;
                                else
                                    return 0;
                            }
                            else
                            {
                                if (index &amp;lt; 10)
                                    return 8;
                                else
                                    return 0;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 14)
                            {
                                if (index &amp;lt; 13)
                                    return 1;
                                else
                                    return 0;
                            }
                            else
                            {
                                if (index &amp;lt; 162)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                    }
                    else
                    {
                        if (index &amp;lt; 178)
                        {
                            if (index &amp;lt; 169)
                            {
                                if (index &amp;lt; 167)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 175)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 182)
                            {
                                if (index &amp;lt; 180)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                return 1;
                            }
                        }
                    }
                }
                else
                {
                    if (index &amp;lt; 450)
                    {
                        if (index &amp;lt; 247)
                        {
                            if (index &amp;lt; 215)
                            {
                                if (index &amp;lt; 184)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 216)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 449)
                            {
                                if (index &amp;lt; 248)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                return 2;
                            }
                        }
                    }
                    else
                    {
                        if (index &amp;lt; 716)
                        {
                            if (index &amp;lt; 712)
                            {
                                if (index &amp;lt; 711)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 713)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 730)
                            {
                                if (index &amp;lt; 729)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                return 1;
                            }
                        }
                    }
                }
            }
            else
            {
                if (index &amp;lt; 8209)
                {
                    if (index &amp;lt; 1025)
                    {
                        if (index &amp;lt; 945)
                        {
                            if (index &amp;lt; 931)
                            {
                                if (index &amp;lt; 930)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 938)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 963)
                            {
                                if (index &amp;lt; 962)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 970)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                    }
                    else
                    {
                        if (index &amp;lt; 1105)
                        {
                            if (index &amp;lt; 1040)
                            {
                                if (index &amp;lt; 1026)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 1104)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 8208)
                            {
                                if (index &amp;lt; 1106)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                return 2;
                            }
                        }
                    }
                }
                else
                {
                    if (index &amp;lt; 8229)
                    {
                        if (index &amp;lt; 8218)
                        {
                            if (index &amp;lt; 8215)
                            {
                                if (index &amp;lt; 8211)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 8216)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 8222)
                            {
                                if (index &amp;lt; 8220)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                return 1;
                            }
                        }
                    }
                    else
                    {
                        if (index &amp;lt; 8242)
                        {
                            if (index &amp;lt; 8240)
                            {
                                if (index &amp;lt; 8231)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 8241)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 8245)
                            {
                                if (index &amp;lt; 8244)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                return 2;
                            }
                        }
                    }
                }
            }
        }
        else
        {
            if (index &amp;lt; 8721)
            {
                if (index &amp;lt; 8481)
                {
                    if (index &amp;lt; 8452)
                    {
                        if (index &amp;lt; 8255)
                        {
                            if (index &amp;lt; 8252)
                            {
                                if (index &amp;lt; 8251)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 8254)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 8365)
                            {
                                if (index &amp;lt; 8364)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 8451)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                    }
                    else
                    {
                        if (index &amp;lt; 8458)
                        {
                            if (index &amp;lt; 8454)
                            {
                                if (index &amp;lt; 8453)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 8457)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 8471)
                            {
                                if (index &amp;lt; 8470)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                return 1;
                            }
                        }
                    }
                }
                else
                {
                    if (index &amp;lt; 8596)
                    {
                        if (index &amp;lt; 8560)
                        {
                            if (index &amp;lt; 8544)
                            {
                                if (index &amp;lt; 8482)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 8556)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 8592)
                            {
                                if (index &amp;lt; 8570)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                return 2;
                            }
                        }
                    }
                    else
                    {
                        if (index &amp;lt; 8713)
                        {
                            if (index &amp;lt; 8602)
                            {
                                if (index &amp;lt; 8598)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 8712)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 8720)
                            {
                                if (index &amp;lt; 8719)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                return 1;
                            }
                        }
                    }
                }
            }
            else
            {
                if (index &amp;lt; 8743)
                {
                    if (index &amp;lt; 8731)
                    {
                        if (index &amp;lt; 8728)
                        {
                            if (index &amp;lt; 8725)
                            {
                                if (index &amp;lt; 8722)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 8726)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 8730)
                            {
                                if (index &amp;lt; 8729)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                return 2;
                            }
                        }
                    }
                    else
                    {
                        if (index &amp;lt; 8740)
                        {
                            if (index &amp;lt; 8737)
                            {
                                if (index &amp;lt; 8733)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 8739)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 8742)
                            {
                                if (index &amp;lt; 8741)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                return 1;
                            }
                        }
                    }
                }
                else
                {
                    if (index &amp;lt; 8766)
                    {
                        if (index &amp;lt; 8756)
                        {
                            if (index &amp;lt; 8750)
                            {
                                if (index &amp;lt; 8748)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 8751)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 8764)
                            {
                                if (index &amp;lt; 8760)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                return 2;
                            }
                        }
                    }
                    else
                    {
                        if (index &amp;lt; 8781)
                        {
                            if (index &amp;lt; 8777)
                            {
                                if (index &amp;lt; 8776)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 8780)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 8787)
                            {
                                if (index &amp;lt; 8786)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                return 1;
                            }
                        }
                    }
                }
            }
        }
    }
    else
    {
        if (index &amp;lt; 12549)
        {
            if (index &amp;lt; 9676)
            {
                if (index &amp;lt; 8979)
                {
                    if (index &amp;lt; 8857)
                    {
                        if (index &amp;lt; 8814)
                        {
                            if (index &amp;lt; 8804)
                            {
                                if (index &amp;lt; 8802)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 8808)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 8853)
                            {
                                if (index &amp;lt; 8816)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 8854)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                    }
                    else
                    {
                        if (index &amp;lt; 8895)
                        {
                            if (index &amp;lt; 8869)
                            {
                                if (index &amp;lt; 8858)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 8870)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 8978)
                            {
                                if (index &amp;lt; 8896)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                return 2;
                            }
                        }
                    }
                }
                else
                {
                    if (index &amp;lt; 9650)
                    {
                        if (index &amp;lt; 9372)
                        {
                            if (index &amp;lt; 9322)
                            {
                                if (index &amp;lt; 9312)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 9332)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 9634)
                            {
                                if (index &amp;lt; 9632)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                return 1;
                            }
                        }
                    }
                    else
                    {
                        if (index &amp;lt; 9670)
                        {
                            if (index &amp;lt; 9660)
                            {
                                if (index &amp;lt; 9652)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 9662)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 9675)
                            {
                                if (index &amp;lt; 9672)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                return 2;
                            }
                        }
                    }
                }
            }
            else
            {
                if (index &amp;lt; 12293)
                {
                    if (index &amp;lt; 9738)
                    {
                        if (index &amp;lt; 9702)
                        {
                            if (index &amp;lt; 9680)
                            {
                                if (index &amp;lt; 9678)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 9698)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 9735)
                            {
                                if (index &amp;lt; 9733)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 9737)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                    }
                    else
                    {
                        if (index &amp;lt; 9795)
                        {
                            if (index &amp;lt; 9793)
                            {
                                if (index &amp;lt; 9792)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 9794)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 12292)
                            {
                                if (index &amp;lt; 12288)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                return 1;
                            }
                        }
                    }
                }
                else
                {
                    if (index &amp;lt; 12436)
                    {
                        if (index &amp;lt; 12321)
                        {
                            if (index &amp;lt; 12317)
                            {
                                if (index &amp;lt; 12312)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 12319)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 12353)
                            {
                                if (index &amp;lt; 12330)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                return 2;
                            }
                        }
                    }
                    else
                    {
                        if (index &amp;lt; 12535)
                        {
                            if (index &amp;lt; 12447)
                            {
                                if (index &amp;lt; 12443)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 12449)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 12543)
                            {
                                if (index &amp;lt; 12540)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                return 1;
                            }
                        }
                    }
                }
            }
        }
        else
        {
            if (index &amp;lt; 55297)
            {
                if (index &amp;lt; 13215)
                {
                    if (index &amp;lt; 12959)
                    {
                        if (index &amp;lt; 12832)
                        {
                            if (index &amp;lt; 12690)
                            {
                                if (index &amp;lt; 12586)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 12704)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 12928)
                            {
                                if (index &amp;lt; 12868)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 12958)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                    }
                    else
                    {
                        if (index &amp;lt; 13198)
                        {
                            if (index &amp;lt; 12969)
                            {
                                if (index &amp;lt; 12964)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 12977)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 13212)
                            {
                                if (index &amp;lt; 13200)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                return 2;
                            }
                        }
                    }
                }
                else
                {
                    if (index &amp;lt; 13265)
                    {
                        if (index &amp;lt; 13253)
                        {
                            if (index &amp;lt; 13218)
                            {
                                if (index &amp;lt; 13217)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 13252)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 13263)
                            {
                                if (index &amp;lt; 13262)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                return 1;
                            }
                        }
                    }
                    else
                    {
                        if (index &amp;lt; 19968)
                        {
                            if (index &amp;lt; 13269)
                            {
                                if (index &amp;lt; 13267)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 13270)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 55296)
                            {
                                if (index &amp;lt; 40870)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                return 0;
                            }
                        }
                    }
                }
            }
            else
            {
                if (index &amp;lt; 65093)
                {
                    if (index &amp;lt; 63733)
                    {
                        if (index &amp;lt; 59335)
                        {
                            if (index &amp;lt; 56321)
                            {
                                if (index &amp;lt; 56320)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 57344)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 59493)
                            {
                                if (index &amp;lt; 59337)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                return 1;
                            }
                        }
                    }
                    else
                    {
                        if (index &amp;lt; 65072)
                        {
                            if (index &amp;lt; 63744)
                            {
                                if (index &amp;lt; 63734)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 64046)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 65075)
                            {
                                if (index &amp;lt; 65074)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                return 2;
                            }
                        }
                    }
                }
                else
                {
                    if (index &amp;lt; 65128)
                    {
                        if (index &amp;lt; 65112)
                        {
                            if (index &amp;lt; 65107)
                            {
                                if (index &amp;lt; 65097)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                if (index &amp;lt; 65108)
                                    return 1;
                                else
                                    return 2;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 65127)
                            {
                                if (index &amp;lt; 65113)
                                    return 1;
                                else
                                    return 2;
                            }
                            else
                            {
                                return 1;
                            }
                        }
                    }
                    else
                    {
                        if (index &amp;lt; 65504)
                        {
                            if (index &amp;lt; 65281)
                            {
                                if (index &amp;lt; 65132)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                if (index &amp;lt; 65375)
                                    return 2;
                                else
                                    return 1;
                            }
                        }
                        else
                        {
                            if (index &amp;lt; 65536)
                            {
                                if (index &amp;lt; 65510)
                                    return 2;
                                else
                                    return 1;
                            }
                            else
                            {
                                return 0;
                            }
                        }
                    }
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者, 我们将这个列表, 写入到byte[], 每一个字符占1bit, 如果对应bit是1, 则表示字符宽度为2, 如果对应位是0, 则表示字符宽度为1. 这样, &lt;strong&gt;速度是更快的&lt;/strong&gt;, 只不过要&lt;strong&gt;牺牲8kb的空间&lt;/strong&gt;了.&lt;/p&gt;
&lt;p&gt;因为控制台中, 字符宽度只有4中可能, 0, 1, 2, 8, 其中0是\r\n\0这类无宽度的, 显然它们也无法参与字符串宽度判断, 1就是普通半角字符, 2就是中文以及全角字符, 8显然是\t, 故, 我们只需要能够表示1和2即可, 也就是1bit的空间.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int[] lens = new int[char.MaxValue + 1]
void WriteData()    // 将已经生成好的宽度数据写入到文件
{
    FileStream fs = File.OpenWrite(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), &quot;Lengths.bin&quot;));
    byte[] write = new byte[1];
    for (int i = 0; i &amp;lt; lens.Length;)
    {
        for (int j = 0; j &amp;lt; 8 &amp;amp;&amp;amp; i &amp;lt; lens.Length; i++, j++)
        {
            if (lens[i] == 2)
                write[0] |= (byte)(1 &amp;lt;&amp;lt; j);
        }
        fs.Write(write, 0, 1);
    }
    fs.Flush();
    fs.Close();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;char.MaxValue / 8 = 8k, 8kb即可装下所有字符的宽度信息.&lt;/p&gt;
&lt;p&gt;然后结合位运算, 就能得出一个简单的函数. 例如 Lengths 是我们的byte数组.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static byte[] Lengths = Resources.Lengths;
public static bool IsWideDisplayCharEx(char c)
{
    int index = c;
    return (Lengths[index / 8] &amp;amp; (1 &amp;lt;&amp;lt; (index % 8))) != 0;
}
public static int GetCharDisplayLengthEx(char c)
{
    return IsWideDisplayCharEx(c) ? 2 : 1;
}
public static int GetStringDisplayLengthEx(string str)
{
    int total = 0;
    foreach (char c in str)
        total += GetCharDisplayLengthEx(c);
    return total;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关于使用:&lt;/h3&gt;
&lt;p&gt;直接去clone开头所写明的代码仓库即可, 会不定时更新的.或者, 直接复制上面的代码(最长的那一段).&lt;/p&gt;
&lt;p&gt;上面的代码大部分是自动生成的哦~&lt;img src=&quot;/images/20210204201122821.png&quot; alt=&quot;偷懒&quot; /&gt;
关于图中使用到的Null.TextEditor, 在这里:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;博客链接: &lt;a href=&quot;/posts/20210203064309/&quot;&gt;使用IronPython来创建支持脚本编辑文本内容的文本编辑器&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Github 仓库地址: &lt;a href=&quot;https://github.com/SlimeNull/Null.TextEditor&quot;&gt;SlimeNull/Null.TextEditor&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;p&gt;点个赞, 留个评论再走吧QAQ&lt;/p&gt;
</content:encoded></item><item><title>C# 循环所有可能的字符</title><link>https://slimenull.com/posts/20201026155944/</link><guid isPermaLink="true">https://slimenull.com/posts/20201026155944/</guid><description>通过 char.MaxValue 来作为循环结尾, 将int强制转换为char, 即可之前自己搜索这个内容, 发现国内没有, 所以写了这个文章供参考for (int i = 0; i &lt;= char.MaxValue; i++){    // 此处放处理语句, (char)i 即为当前字符}...</description><pubDate>Mon, 26 Oct 2020 15:59:44 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;通过 char.MaxValue 来作为循环结尾, 将int强制转换为char, 即可
&lt;s&gt;之前自己搜索这个内容, 发现国内没有, 所以写了这个文章供参考&lt;/s&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 0; i &amp;lt;= char.MaxValue; i++)
{
    // 此处放处理语句, (char)i 即为当前字符
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C#] 各种关于类型与反射的常用操作. 类型操作, 根据Type进行类型转换, 接口获取与比较, 泛型接口获取与比较, 类的字段获取与设置.</title><link>https://slimenull.com/posts/20201019211601/</link><guid isPermaLink="true">https://slimenull.com/posts/20201019211601/</guid><description>直切入正题本文章是面向初学者的一些资料注意: 存在即合理, 可能某些初学者认为这些东西并无必要, 但实际上它们有很大的用处获取类型(Type)对象object obj;Type objType = obj.GetType();判断类型是否可以转换这个方法同样可以判断B是否继承于A(可以是类和接口), 但是如果你要判断一个类是否继承了一个泛型接口, 不指定相同的泛型参数, 是无法判断成功的, 比如一个继承了Demo&lt;string, string&gt;接口的类在使用下面的方法来判断</description><pubDate>Mon, 19 Oct 2020 21:16:01 GMT</pubDate><content:encoded>&lt;h1&gt;直切入正题&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本文章是面向初学者的一些资料
注意: 存在即合理, 可能某些初学者认为这些东西并无必要, 但实际上它们有很大的用处&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;获取类型(Type)对象&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;object obj;
Type objType = obj.GetType();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;判断类型是否可以转换&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;这个方法同样可以判断B是否继承于A(可以是类和接口), 但是如果你要判断一个类是否继承了一个泛型接口, 不指定相同的泛型参数, 是无法判断成功的, 比如一个继承了Demo&amp;lt;string, string&amp;gt;接口的类在使用下面的方法来判断是否继承Demo&amp;lt;&amp;gt;接口时, 就无法获得正确的结果. 这种方法是严谨的.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例子表示了A是否派生B, 也就是B是否继承于A, 或者说B是否可以强制转换为A&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool result = typeof(B).IsAssignableFrom(typeof(B));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;获取类型继承的的接口 (包含泛型接口)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;获取的接口, 都是接口原型, 比如获取到的会是IDicrionary&amp;lt;,&amp;gt;, 而不是IDictionary&amp;lt;TKey, TValue&amp;gt;. 利用这个特性, 我们可以判断一个类型是否继承某个泛型接口, 而不需要指定详细的泛型参数.&lt;/li&gt;
&lt;li&gt;下面的原始接口指IDictionary&amp;lt;string,string&amp;gt;与IDictionary&amp;lt;,&amp;gt;之间的关系, IDictionary即原始接口 (这个概念并不是公认概念, 但是没有已经规定的概念来描述&quot;原始接口&quot;, 所以在这里提出了这个概念)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;Type[] interfaces = typeof(A).GetInterfaces();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考: 判断是否继承某个泛型接口并获取泛型参数:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 在这个方法中, 会比对targetType的原始接口是否与interfaceType一致
private static bool CheckInterface(Type targetType, Type interfaceType, out Type[] geneticTypes)
{
    foreach(Type i in targetType.GetInterfaces())
    {
        if (i.GetGenericTypeDefinition().Equals(interfaceType))  // GetGenericTypeDefinition() 即获取原始接口类型. 如果去除这个方法, 则是严谨的比较类型
        {
            geneticTypes = i.GetGenericArguments();
            return true;
        }
    }
    geneticTypes = null;
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;类的字段获取与设置&lt;/h3&gt;
&lt;p&gt;以类型A为例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A obj = new A();
Type objType = typeof(A);
FieldInfo[] fieldInfos = objType.GetFields();     // 获取字段信息
object fieldValue = fieldInfos[0].GetValue(obj);  // 获取字段值
fieldInfos[0].SetValue(obj, &quot;这是一个值&quot;);         // 设置字段的值
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;根据Type对象进行类型转换&lt;/h3&gt;
&lt;p&gt;例如, 将A转换为B&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A objA = new A();
object objB = Convert.ChangeType(objA, typeof(B));    // 返回了转换为B类型的对象的引用
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>记录一次经验:Image.Save遇到A generic error occurred in GDI+异常</title><link>https://slimenull.com/posts/20200826215919/</link><guid isPermaLink="true">https://slimenull.com/posts/20200826215919/</guid><description>要点:先看自己路径是不是错了然后看自己的路径表达是不是不符合标准, 例如这样的&quot;/ewq_00010.png&quot;, 它指向C:\根目录下的一个文件! 而.NET中一般不允许对那里进行写操作然后, 我就是在上述内容的情况上犯了错… “/ewq_00010.png&quot;应该是”./ewq_00010.png&quot;, 使用这个点来表示, 它是一个相对路径我的解决过程Image 保存的Path是&quot;/ewq_00010.png&quot;在发现这个问题后, 我首先是检查了一下, 我写的路径是否是正确的, 比如, 目</description><pubDate>Wed, 26 Aug 2020 21:59:19 GMT</pubDate><content:encoded>&lt;h2&gt;要点:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;先看自己路径是不是错了&lt;/li&gt;
&lt;li&gt;然后看自己的路径表达是不是不符合标准, 例如这样的&quot;/ewq_00010.png&quot;, 它指向C:\根目录下的一个文件! 而.NET中一般不允许对那里进行写操作&lt;/li&gt;
&lt;li&gt;然后, 我就是在上述内容的情况上犯了错... &quot;/ewq_00010.png&quot;应该是&quot;./ewq_00010.png&quot;, 使用这个点来表示, 它是一个相对路径&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;我的解决过程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Image 保存的Path是&quot;/ewq_00010.png&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;在发现这个问题后, 我首先是检查了一下, 我写的路径是否是正确的, 比如, 目录是否存在, 我使用VS的调试功能, 测试了一下, Directory.Exists(Path.GetDirectoryName(&quot;/ewq_00010.png&quot;)) 结果是true, 初步判断不是路径问题&lt;/li&gt;
&lt;li&gt;后来查资料, 发现可能是这个Image是FromFile生成的而报错, 但我的不是, 它完完全全是一个new出来的, 我又尝试性地指定ImageFormat.Png, 果然, 问题没有解决&lt;/li&gt;
&lt;li&gt;继续查资料, 发现有人使用FileStream解决了问题, 我也尝试, 然后失败了... 写入被拒绝, 查了查资料, 为什么FileStream写入被拒绝, 一堆乱七八糟的东西, 看都看不懂, 大概就是讲ASP.NET不能在C:\写入, 但是我又不是ASP.NET&lt;/li&gt;
&lt;li&gt;然后我尝试转个弯子, 用MemoryStream, 然后最后用File.WriteAllBytes写入试试, 最后, 我突然发现, 它提示我, 对C:/的写入被拒绝, 蛤??? 我的路径不是&quot;/ewq_0010.png&quot;吗??? 我脑子跟Linux联想了起来, 总不会... 单个斜杠指C:/吧?&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;这个输出路径, 其实是经过我的一个PathCombine函数拼合而成的路径, 它很简单, 但也因为它的判断很简单而嗝屁了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public static string CombinePath(string path1, string path2)  // 简单粗暴路径拼合
{
    if (path2.Contains(&apos;:&apos;))
    {
        throw new ArgumentException(&quot;第二个路径不可是绝对路径&quot;);
    }
    return path1.TrimEnd(new char[] { &apos;/&apos;, &apos;\\&apos; }).Replace(&apos;\\&apos;, &apos;/&apos;) + &quot;/&quot; + path2.TrimStart(new char[] { &apos;/&apos;, &apos;\\&apos; }).Replace(&apos;\\&apos;, &apos;/&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;然后我把这个函数改成了这样:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public static string CombinePath(string path1, string path2)
{
    if (path2.Contains(&apos;:&apos;))
    {
        throw new ArgumentException(&quot;第二个路径不可是绝对路径&quot;);
    }
    string result = path1.TrimEnd(new char[] { &apos;/&apos;, &apos;\\&apos; }).Replace(&apos;\\&apos;, &apos;/&apos;) + &quot;/&quot; + path2.TrimStart(new char[] { &apos;/&apos;, &apos;\\&apos; }).Replace(&apos;\\&apos;, &apos;/&apos;);
    if (result.Contains(&apos;:&apos;))     // 简单粗暴判断你是否是绝对路径
    {
        return result;   // 如果是绝对路径就直接返回, 也不管他是否是对的
    }
    else
    {
        return &quot;./&quot; + result.Trim(&apos;/&apos;);   // 如果不是, 则加一个&quot;./&quot;来强调, &apos;我这是相对路径&apos;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;然后... 程序成功跑起来了...&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/2020082621482447.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;最后, 这个程序是用于根据atlas图像索引表分解图像的, 如果你觉得有参考价值, 可以私信我
&lt;img src=&quot;/images/20200826215228152.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>[C#] 一个类实现拖拽调整窗体或控件大小</title><link>https://slimenull.com/posts/20200710031128/</link><guid isPermaLink="true">https://slimenull.com/posts/20200710031128/</guid><description>最近闲来无事, 倒是借助WebAPI实现翻译器, 本想设计一个炫酷的界面(模仿VS), 却没想到, 难度大大超出我的想象, 拖拽, 调整大小, 如果要实现VS的边框, 还需要想办法做到过渡透明! 这对于WinForm来说实在是太难了, 如果不过渡透明, 就是全透明, 那鼠标就直接穿窗体了!不过还是有些成果的, 比如, 造了两个轮子(我真是一个热衷于造轮子的傻子)所说的轮子就是文章标题咯, 因为之前我还做了一个类来实现拖拽移动控件或窗体嘛, 所以我就直接把这个调整大小的跟之前的功能整合到了..</description><pubDate>Fri, 10 Jul 2020 03:11:28 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/images/2020071002573582.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;最近闲来无事, 倒是借助WebAPI实现翻译器, 本想设计一个炫酷的界面(模仿VS), 却没想到, 难度大大超出我的想象, 拖拽, 调整大小, 如果要实现VS的边框, 还需要想办法做到过渡透明! 这对于WinForm来说实在是太难了, 如果不过渡透明, 就是全透明, 那鼠标就直接穿窗体了!&lt;/li&gt;
&lt;li&gt;不过还是有些成果的, 比如, 造了两个轮子&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;(我真是一个热衷于造轮子的傻子)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;所说的轮子就是文章标题咯, 因为之前我还做了一个类来实现拖拽移动控件或窗体嘛, 所以我就直接把这个调整大小的跟之前的功能整合到了一起. (可以提到的是, 期间因为特殊需求, 还做了一个映射拖拽移动控件窗体, 就是拖拽控件1, 移动控件2)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下面的代码就是程序主体了, 它是应该被封装到一个类库里的, 所以, 不要直接把这些代码添加到你的文件中, 而是新建一个类库(不是新建项目, 而是添加到你的项目中), 然后把这些代码覆盖到新的文件中, 在你的程序中using CHO.DragOperation即可使用代码所包含的3个功能( ~DragMove,MapDragMove,DragResize~ )&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace CHO
{
    namespace DragOperation
    {
        public class DragMove
        {
            public DragMove(MouseButtons MouseButton, DragMoveMode FollowMode = DragMoveMode.All, bool OutParent = false)
            {
                this.MouseButton = MouseButton;
                this.FollowMode = FollowMode;
                this.OutParent = OutParent;
            }
            public DragMove(DragMoveMode FollowMode = DragMoveMode.All, bool OutParent = false)
            {
                AllMouseButton = true;
                this.FollowMode = FollowMode;
                this.OutParent = OutParent;
            }

            object AboutControl;                                  // 表示当前移动控件操作是针对哪个控件的
            bool Moving = false;                                  // 表示是否正在移动控件
            bool AllMouseButton = false;                          // 表示是否处理所有按钮
            bool OutParent;                                       // 表示是否允许脱离父容器的显示区域
            Point ControlStartLocation;                           // 表示移动操作时, 控件的初始位置
            Point MouseFirstLocationAboutParent;                  // 表示移动操作时, 鼠标的初始位置
            DragMoveMode FollowMode;                              // 跟随模式
            MouseButtons MouseButton;                             // 判定按钮
            List&amp;lt;Control&amp;gt; AddedControls = new List&amp;lt;Control&amp;gt;();    // 已添加控件


            void MouseDownEvent(object sender, MouseEventArgs e)
            {
                if ((e.Button == MouseButton) || AllMouseButton)
                {
                    AboutControl = sender;
                    Moving = true;
                    ControlStartLocation = (sender as Control).Location;
                    if (sender is Form)
                    {
                        MouseFirstLocationAboutParent = Control.MousePosition;
                    }
                    else
                    {
                        MouseFirstLocationAboutParent = (sender as Control).Parent.PointToClient(Control.MousePosition);
                    }
                }
            }
            void MouseMoveEvent(object sender, MouseEventArgs e)
            {
                if (sender == AboutControl)
                {
                    if (Moving)
                    {
                        Point MouseLocationNow;
                        if (sender is Form)
                        {
                            MouseLocationNow = Control.MousePosition;
                        }
                        else
                        {
                            MouseLocationNow = (sender as Control).Parent.PointToClient(Control.MousePosition);
                        }
                        int NewX = MouseLocationNow.X - MouseFirstLocationAboutParent.X + ControlStartLocation.X;
                        int NewY = MouseLocationNow.Y - MouseFirstLocationAboutParent.Y + ControlStartLocation.Y;
                        if (!OutParent)
                        {
                            if (!(sender is Form))
                            {
                                if (NewX &amp;lt; 0)
                                {
                                    NewX = 0;
                                }
                                else if (NewX + (sender as Control).Width &amp;gt; (sender as Control).Parent.Width)
                                {
                                    NewX = (sender as Control).Parent.Width - (sender as Control).Width;
                                }
                                if (NewY &amp;lt; 0)
                                {
                                    NewY = 0;
                                }
                                else if (NewY + (sender as Control).Height &amp;gt; (sender as Control).Parent.Height)
                                {
                                    NewY = (sender as Control).Parent.Height - (sender as Control).Height;
                                }
                            }
                        }
                        if (FollowMode == DragMoveMode.X)
                        {
                            NewY = ControlStartLocation.Y;
                        }
                        if (FollowMode == DragMoveMode.Y)
                        {
                            NewX = ControlStartLocation.X;
                        }
                        (sender as Control).Location = new Point(NewX, NewY);
                    }
                }
                else
                {
                    if (Moving)
                    {
                        Moving = false;
                        (AboutControl as Control).Location = ControlStartLocation;
                    }
                }
            }
            void MouseUpEvent(object sender, MouseEventArgs e)
            {
                if (sender != AboutControl)
                {
                    if (Moving)
                    {
                        (AboutControl as Control).Location = ControlStartLocation;
                    }
                }
                Moving = false;
            }
            public bool AddControl(Control control)
            {
                if (AddedControls.Contains(control))
                {
                    return false;
                }
                else
                {
                    control.MouseDown += MouseDownEvent;
                    control.MouseMove += MouseMoveEvent;
                    control.MouseUp += MouseUpEvent;
                    AddedControls.Add(control);
                    return true;
                }
            }
            public bool RemoveControl(Control control)
            {
                if (AddedControls.Contains(control))
                {
                    control.MouseDown -= MouseDownEvent;
                    control.MouseMove -= MouseMoveEvent;
                    control.MouseUp -= MouseUpEvent;
                    AddedControls.Remove(control);
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }

        public class MapDragMove
        {
            public MapDragMove(MouseButtons MouseButton, DragMoveMode FollowMode = DragMoveMode.All)
            {
                this.MouseButton = MouseButton;
                this.FollowMode = FollowMode;
            }
            public MapDragMove(DragMoveMode FollowMode = DragMoveMode.All)
            {
                AllMouseButton = true;
                this.FollowMode = FollowMode;
            }

            object AboutControl;                                  // 表示当前移动控件操作是针对哪个控件的
            bool Moving = false;                                  // 表示是否正在移动控件
            bool AllMouseButton = false;                          // 表示是否处理所有按钮
            Point ControlStartLocation;                           // 表示移动操作时, 控件的初始位置
            Point MouseFirstLocation;                             // 表示移动操作时, 鼠标的初始位置
            DragMoveMode FollowMode;                              // 跟随模式
            MouseButtons MouseButton;                             // 判定按钮
            Dictionary&amp;lt;Control, Control&amp;gt; AddedControls = new Dictionary&amp;lt;Control, Control&amp;gt;();    // 已添加控件


            void MouseDownEvent(object sender, MouseEventArgs e)
            {
                if ((e.Button == MouseButton) || AllMouseButton)
                {
                    AboutControl = sender;
                    Moving = true;
                    ControlStartLocation = (sender as Control).Location;
                    MouseFirstLocation = Control.MousePosition;
                }
            }
            void MouseMoveEvent(object sender, MouseEventArgs e)
            {
                if (sender == AboutControl)
                {
                    if (Moving)
                    {
                        Point MouseLocationNow = Control.MousePosition;

                        if (FollowMode == DragMoveMode.X)
                        {
                            AddedControls[sender as Control].Left += MouseLocationNow.X - MouseFirstLocation.X;
                        }
                        else if (FollowMode == DragMoveMode.Y)
                        {
                            AddedControls[sender as Control].Top += MouseLocationNow.Y - MouseFirstLocation.Y;
                        }
                        else
                        {
                            AddedControls[sender as Control].Location = new Point(AddedControls[sender as Control].Location.X + (MouseLocationNow.X - MouseFirstLocation.X), AddedControls[sender as Control].Location.Y + (MouseLocationNow.Y - MouseFirstLocation.Y));
                        }

                        MouseFirstLocation = MouseLocationNow;
                    }
                }
                else
                {
                    if (Moving)
                    {
                        Moving = false;
                        (AboutControl as Control).Location = ControlStartLocation;
                    }
                }
            }
            void MouseUpEvent(object sender, MouseEventArgs e)
            {
                if (sender != AboutControl)
                {
                    if (Moving)
                    {
                        (AboutControl as Control).Location = ControlStartLocation;
                    }
                }
                Moving = false;
            }
            public bool AddControl(Control dragControl, Control moveControl)
            {
                if (AddedControls.ContainsKey(dragControl))
                {
                    return false;
                }
                else
                {
                    dragControl.MouseDown += MouseDownEvent;
                    dragControl.MouseMove += MouseMoveEvent;
                    dragControl.MouseUp += MouseUpEvent;
                    AddedControls.Add(dragControl, moveControl);
                    return true;
                }
            }
            public bool RemoveControl(Control control)
            {
                if (AddedControls.ContainsKey(control))
                {
                    control.MouseDown -= MouseDownEvent;
                    control.MouseMove -= MouseMoveEvent;
                    control.MouseUp -= MouseUpEvent;
                    AddedControls.Remove(control);
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }

        /// &amp;lt;summary&amp;gt;
        /// 控件跟随鼠标的移动模式
        /// &amp;lt;/summary&amp;gt;
        public enum DragMoveMode
        {
            /// &amp;lt;summary&amp;gt;
            /// 表示只跟随横坐标
            /// &amp;lt;/summary&amp;gt;
            X,
            /// &amp;lt;summary&amp;gt;
            /// 表示只跟随纵坐标
            /// &amp;lt;/summary&amp;gt;
            Y,
            /// &amp;lt;summary&amp;gt;
            /// 跟随横坐标与纵坐标
            /// &amp;lt;/summary&amp;gt;
            All
        }

        public class DragResize
        {
            public DragResize(MouseButtons MouseButton, int BorderSize = 5)
            {
                this.MouseButton = MouseButton;
                this.BorderSize = BorderSize;
            }
            public DragResize(int BorderSize = 5)
            {
                this.AllMouseButton = true;
                this.BorderSize = BorderSize;
            }

            int BorderSize = 10;                                  // 边框判断大小
            object AboutControl;                                  // 表示当前调整控件操作是针对哪个控件的
            bool Resizing = false;                                // 表示是否正在调整控件
            bool AllMouseButton = false;                          // 表示是否处理所有按钮

            bool MouseDownING = false;

            bool LeftResize;
            bool RightResize;
            bool TopResize;
            bool BottomResize;
            private Point MouseFirstLocation;                     // 表示调整操作时, 鼠标的初始位置
            MouseButtons MouseButton;                             // 判定按钮
            List&amp;lt;Control&amp;gt; AddedControls = new List&amp;lt;Control&amp;gt;();    // 已添加控件

            void CheckMouse(object sender)
            {
                Point mousePosition;
                if (sender is Form)
                {
                    mousePosition = Control.MousePosition;
                }
                else
                {
                    mousePosition = (sender as Control).Parent.PointToClient(Control.MousePosition);
                }

                if (mousePosition.X &amp;gt;= (sender as Form).Left &amp;amp;&amp;amp; mousePosition.X &amp;lt;= (sender as Form).Left + BorderSize)
                {
                    LeftResize = true;
                }
                else if (mousePosition.X &amp;gt;= (sender as Form).Left + (sender as Form).Width - BorderSize &amp;amp;&amp;amp; mousePosition.X &amp;lt;= (sender as Form).Left + (sender as Form).Width)
                {
                    RightResize = true;
                }
                else
                {
                    LeftResize = false;
                    RightResize = false;
                }
                if (mousePosition.Y &amp;lt;= (sender as Form).Top + BorderSize &amp;amp;&amp;amp; mousePosition.Y &amp;gt;= (sender as Form).Top)
                {
                    TopResize = true;
                }
                else if (mousePosition.Y &amp;gt;= (sender as Form).Top + (sender as Form).Height - BorderSize &amp;amp;&amp;amp; mousePosition.Y &amp;lt;= (sender as Form).Top + (sender as Form).Height)
                {
                    BottomResize = true;
                }
                else
                {
                    TopResize = false;
                    BottomResize = false;
                }

                if ((LeftResize || RightResize || TopResize || BottomResize) &amp;amp;&amp;amp; MouseDownING)
                {
                    Resizing = true;
                    AboutControl = sender;
                }
                else
                {
                    Resizing = false;
                }
            }

            void MouseDownEvent(object sender, MouseEventArgs e)
            {
                if (AllMouseButton || e.Button == MouseButton)
                {
                    MouseDownING = true;
                    CheckMouse(sender);

                    if (Resizing)
                    {
                        MouseFirstLocation = Control.MousePosition;
                    }
                }
            }
            void MouseMoveEvent(object sender, MouseEventArgs e)
            {
                if (Resizing &amp;amp;&amp;amp; sender == AboutControl)
                {
                    Point mousePosition = Control.MousePosition;
                    (sender as Control).Invalidate(false);
                    if (LeftResize)
                    {
                        (sender as Control).Width += MouseFirstLocation.X - mousePosition.X;
                        (sender as Control).Left -= MouseFirstLocation.X - mousePosition.X;
                    }
                    else if (RightResize)
                    {
                        (sender as Control).Width += mousePosition.X - MouseFirstLocation.X;
                    }
                    if (TopResize)
                    {
                        (sender as Control).Height += MouseFirstLocation.Y - mousePosition.Y;
                        (sender as Control).Top -= MouseFirstLocation.Y - mousePosition.Y;
                    }
                    else if (BottomResize)
                    {
                        (sender as Control).Height += mousePosition.Y - MouseFirstLocation.Y;
                    }
                    (sender as Control).Invalidate(true);
                    MouseFirstLocation = mousePosition;
                }
                else
                {
                    CheckMouse(sender);
                    if (TopResize)
                    {
                        if (LeftResize)
                        {
                            (sender as Control).Cursor = Cursors.SizeNWSE;
                        }
                        else if (RightResize)
                        {
                            (sender as Control).Cursor = Cursors.SizeNESW;
                        }
                        else
                        {
                            (sender as Control).Cursor = Cursors.SizeNS;
                        }
                    }
                    else if (BottomResize)
                    {
                        if (LeftResize)
                        {
                            (sender as Control).Cursor = Cursors.SizeNESW;
                        }
                        else if (RightResize)
                        {
                            (sender as Control).Cursor = Cursors.SizeNWSE;
                        }
                        else
                        {
                            (sender as Control).Cursor = Cursors.SizeNS;
                        }
                    }
                    else if (LeftResize)
                    {
                        (sender as Control).Cursor = Cursors.SizeWE;
                    }
                    else if (RightResize)
                    {
                        (sender as Control).Cursor = Cursors.SizeWE;
                    }
                    else
                    {
                        (sender as Control).Cursor = Cursors.Default;
                    }

                }
            }
            void MouseUpEvent(object sender, MouseEventArgs e)
            {
                MouseDownING = false;
                if (Resizing)
                {
                    Resizing = false;
                }
            }
            void MouseLeaveEvent(object sender, EventArgs e)
            {
                (sender as Control).Cursor = Cursors.Default;
            }

            public bool AddControl(Control control)
            {
                if (AddedControls.Contains(control))
                {
                    return false;
                }
                else
                {
                    control.MouseLeave += MouseLeaveEvent;
                    control.MouseDown += MouseDownEvent;
                    control.MouseMove += MouseMoveEvent;
                    control.MouseUp += MouseUpEvent;
                    AddedControls.Add(control);
                    return true;
                }
            }
            public bool RemoveControl(Control control)
            {
                if (AddedControls.Contains(control))
                {
                    control.MouseDown -= MouseDownEvent;
                    control.MouseMove -= MouseMoveEvent;
                    control.MouseUp -= MouseUpEvent;
                    AddedControls.Remove(control);
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C#] 鼠标拖动实现控件移动 - 一个类实现对多个控件与窗体的鼠标拖动移动操作</title><link>https://slimenull.com/posts/20200706065250/</link><guid isPermaLink="true">https://slimenull.com/posts/20200706065250/</guid><description>关于文章:文章包含以下部分:对鼠标拖动实现控件移动的原理详解使用类将功能封装适用于:C# WinForm原理:每当鼠标移动时, 根据鼠标坐标计算出控件应处于的位置并将控件移动到计算出的位置, 另外, 为了标识是否正在拖动控件, 还需要订阅控件的MouseDown和MouseUp事件.当MouseDown事件触发时, 标识是否正在拖动控件的布尔变量设置为true, 当MouseUp事件触发时, 标识是否正在拖动控件的布尔变量设置为false; 在鼠标移动时, 会判断这个变量的值以</description><pubDate>Mon, 06 Jul 2020 06:52:50 GMT</pubDate><content:encoded>&lt;h2&gt;关于文章:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;h6&gt;文章包含以下部分:&lt;/h6&gt;
&lt;/li&gt;
&lt;li&gt;对鼠标拖动实现控件移动的原理详解&lt;/li&gt;
&lt;li&gt;使用类将功能封装&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;适用于:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;C# WinForm&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;更新日志: 2020-07-07 9:05AM 更改了多控件实现的算法, 使其能够支持移动窗体&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;原理:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;每当鼠标移动时, 根据鼠标坐标计算出控件应处于的位置并将控件移动到计算出的位置, 另外, 为了标识是否正在拖动控件, 还需要订阅控件的MouseDown和MouseUp事件.&lt;/li&gt;
&lt;li&gt;当MouseDown事件触发时, 标识是否正在拖动控件的布尔变量设置为true, 当MouseUp事件触发时, 标识是否正在拖动控件的布尔变量设置为false; 在鼠标移动时, 会判断这个变量的值以决定是否应该移动控件.&lt;/li&gt;
&lt;li&gt;为了保证鼠标相对控件的位置是不变的, 所以还需要一个Point类型的变量来标识鼠标按下控件时, 鼠标相对控件的相对位置.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;一些资料:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;获取鼠标相对屏幕的坐标 :  Control.MousePosition;&lt;/li&gt;
&lt;li&gt;获取一个 相对于屏幕的坐标 相对某个控件的相对坐标 : 控件.PointToClient(一个相对于屏幕的坐标);&lt;/li&gt;
&lt;li&gt;
&lt;h6&gt;下面是方便初学者的:&lt;/h6&gt;
&lt;/li&gt;
&lt;li&gt;as运算符, 将某个实例作为某种类型使用, 例如:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// obj是一个Button转换为object类型的按钮
// 那么, (obj as Button).Text = &quot;新的文本&quot;; 是可用的, 它将为控件指定一个新的文本
(obj as Button).Text = &quot;新的文本&quot;;
// 注意, as运算符的优先级低于.运算符, 所以括号不可以省去
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;单控件实现:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;新建项目, 项目类型是WinForm.&lt;/li&gt;
&lt;li&gt;添加控件 (我这里将控件的Name设置为了ExempleButton)&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;浓浓的Chinglish味道, 哈哈哈&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/20200706052109567.png&quot; alt=&quot;添加按钮控件&quot; /&gt;
3. 添加相关事件(MouseDown, MouseMove, MouseUp):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20200706052323882.png&quot; alt=&quot;添加相关事件&quot; /&gt;
4. 转到代码, 添加相关变量.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20200706052556910.png&quot; alt=&quot;添加相关变量&quot; /&gt;
5. 添加关键代码:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/20200706054641301.png&quot; alt=&quot;代码图片&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool Moving = false;                // 标识是否在拖动控件
Point MouseFirstLocation;           // 鼠标按下时, 相对于控件的坐标

private void ExempleButton_MouseDown(object sender, MouseEventArgs e)
{
    Moving = true;                                                   // 表示进入拖动控件的状态
    Point MousePoint = Control.MousePosition;                        // 获取鼠标相对屏幕的坐标
    MouseFirstLocation = ExempleButton.PointToClient(MousePoint);    // 获取坐标相对于控件的相对坐标并赋值给MouseFirstLocation
}

private void ExempleButton_MouseMove(object sender, MouseEventArgs e)
{
    if (Moving)
    {
        Point MousePoint = Control.MousePosition;                                                                                                   // 获取鼠标相对屏幕的坐标
        Point MousePointToContainer = ExempleButton.Parent.PointToClient(MousePoint);                                                               // 获取鼠标相对控件父容器的坐标
        Point ControlNewLocation = new Point(MousePointToContainer.X - MouseFirstLocation.X, MousePointToContainer.Y - MouseFirstLocation.Y);       // 计算控件应处于的, 新的坐标
        ExempleButton.Location = ControlNewLocation;                                                                                                // 移动控件
    }
}

private void ExempleButton_MouseUp(object sender, MouseEventArgs e)
{
    Moving = false;                                                  // 脱离拖动控件的状态
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;可以使用了~&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;多控件实现:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用类封装功能, 类的AddControl(Control control)方法来快捷使某个控件支持鼠标拖动移动位置.&lt;/li&gt;
&lt;li&gt;这里的原理与前面的单控件实现有些出入, 但也是通过初始鼠标坐标(这里是相对父容器而不是移动的控件)与初始坐标计算出的&lt;/li&gt;
&lt;li&gt;这个类不仅支持窗体内的控件, 而且还支持窗体 (没有直接使用WindowsAPI)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class DragMove
{
    public DragMove(MouseButtons MouseButton)
    {
        this.MouseButton = MouseButton;
    }
    public DragMove()
    {
        AllMouseButton = true;
    }

    object AboutControl;                                  // 表示当前移动控件操作是针对哪个控件的
    bool Moving = false;                                  // 表示是否正在移动控件
    bool AllMouseButton = false;                          // 表示是否处理所有按钮
    Point ControlStartLocation;                           // 表示移动操作时, 控件的初始位置
    Point MouseFirstLocationAboutParent;                  // 表示移动操作时, 鼠标的初始位置
    MouseButtons MouseButton;
    List&amp;lt;Control&amp;gt; AddedControls = new List&amp;lt;Control&amp;gt;();

    void MouseDownEvent(object sender, MouseEventArgs e)
    {
        if ((e.Button == MouseButton) || AllMouseButton)
        {
            AboutControl = sender;
            Moving = true;
            ControlStartLocation = (sender as Control).Location;
            if (sender is Form)
            {
                MouseFirstLocationAboutParent = Control.MousePosition;
            }
            else
            {
                MouseFirstLocationAboutParent = (sender as Control).Parent.PointToClient(Control.MousePosition);
            }
        }
    }
    void MouseMoveEvent(object sender, MouseEventArgs e)
    {
        if (sender == AboutControl)
        {
            if (Moving)
            {
                Point MouseLocationNow;
                if (sender is Form)
                {
                    MouseLocationNow = Control.MousePosition;
                }
                else
                {
                    MouseLocationNow = (sender as Control).Parent.PointToClient(Control.MousePosition);
                }
                Point ControlNewLocation = new Point(MouseLocationNow.X - MouseFirstLocationAboutParent.X + ControlStartLocation.X, MouseLocationNow.Y - MouseFirstLocationAboutParent.Y + ControlStartLocation.Y);
                (sender as Control).Location = ControlNewLocation;
            }
        }
        else
        {
            if (Moving)
            {
                Moving = false;
                (AboutControl as Control).Location = ControlStartLocation;
            }
        }
    }
    void MouseUpEvent(object sender, MouseEventArgs e)
    {
        if (sender != AboutControl)
        {
            if (Moving)
            {
                (AboutControl as Control).Location = ControlStartLocation;
            }
        }
        Moving = false;
    }
    public bool AddControl(Control control)
    {
        if (AddedControls.Contains(control))
        {
            return false;
        }
        else
        {
            control.MouseDown += MouseDownEvent;
            control.MouseMove += MouseMoveEvent;
            control.MouseUp += MouseUpEvent;
            AddedControls.Add(control);
            return true;
        }
    }
    public bool RemoveControl(Control control)
    {
        if (AddedControls.Contains(control))
        {
            control.MouseDown -= MouseDownEvent;
            control.MouseMove -= MouseMoveEvent;
            control.MouseUp -= MouseUpEvent;
            AddedControls.Remove(control);
            return true;
        }
        else
        {
            return false;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这个类包含了判断鼠标按键的功能, 比如只允许右键拖动控件, 也有一些预防措施, 保证类能够正常工作的一些措施(判断控件), 也支持添加与移除控件&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[C#] 如何创建DLL并在项目中使用</title><link>https://slimenull.com/posts/20200705010533/</link><guid isPermaLink="true">https://slimenull.com/posts/20200705010533/</guid><description>注意:文章适合初学者, 讲的较为详细, 大佬可以绕道作者也是自学C#的, 所以有些东西可能讲的也有些不好, 请见谅关于DLL:在C#中, DLL可以说是类库, 创建一个类库类型的项目后, 生成时生成的文件时一个DLL文件一个类库中, 包含一个或多个类, 这些类处于某个命名空间下, 当引用这个DLL文件后, using 相应的命名空间后即可直接使用类库中所包含的类一般的, 创建DLL文件为的是将自己定义的一个类, 直接制作成DLL文件以方便别人使用, 这样不需要复制代码也可以使用你定义的类.</description><pubDate>Sun, 05 Jul 2020 01:05:33 GMT</pubDate><content:encoded>&lt;h2&gt;注意:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;文章适合初学者, 讲的较为详细, 大佬可以绕道&lt;/li&gt;
&lt;li&gt;作者也是自学C#的, 所以有些东西可能讲的也有些不好, 请见谅&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;关于DLL:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在C#中, DLL可以说是类库, 创建一个类库类型的项目后, 生成时生成的文件时一个DLL文件&lt;/li&gt;
&lt;li&gt;一个类库中, 包含一个或多个类, 这些类处于某个命名空间下, 当引用这个DLL文件后, using 相应的命名空间后即可直接使用类库中所包含的类&lt;/li&gt;
&lt;li&gt;一般的, 创建DLL文件为的是将自己定义的一个类, 直接制作成DLL文件以方便别人使用, 这样不需要复制代码也可以使用你定义的类.&lt;/li&gt;
&lt;li&gt;(对于不了解引用是什么的初学者, 你需要知道, 一个C#程序, 会使用不少的类库, 比如System.dll, System.Windows.Forms.dll, 但是这些.NET框架含有的类库, C#程序可以直接使用, 而不需要将DLL文件置于程序所在目录下, 项目的&apos;引用&apos;说明了这个程序使用了什么类库, 而且在你编程的时候, VS也会根据你引用的类库来给你代码提示)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;创建DLL文件:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;创建一个 类库(.NET Framework) 项目, 如下图, 不过, 因为我已经有创建过了, 所以我就直接使用之前创建的项目了.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/2020070500212889.png&quot; alt=&quot;创建一个 类库(.NET Framework) 项目&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建完成后, 你就可以写代码了, 要清楚, C#的类库包含的其实就是一个个的类, 或者其他成员(不能是字段).&lt;/li&gt;
&lt;li&gt;下图, 就是我之前的项目, 注意, 这些供使用者访问的成员别忘了使用public修饰.&lt;/li&gt;
&lt;li&gt;我的这个项目, 是一个用来操作Json的类库, 之后将使用它来尝试操作DLL
&lt;img src=&quot;/images/20200705002655466.png&quot; alt=&quot;已经写好代码的类库项目&quot; /&gt;&lt;/li&gt;
&lt;li&gt;生成项目, 并找到生成的DLL文件.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/20200705003106729.png&quot; alt=&quot;生成成功&quot; /&gt;
&lt;img src=&quot;/images/20200705003205677.png&quot; alt=&quot;这就是生成好的DLL文件&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;引用生成的DLL:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;首先, 转到另外一个项目, 我们将在这个项目中使用之前生成的DLL.
&lt;img src=&quot;/images/20200705003504505.png&quot; alt=&quot;一个新的控制台项目&quot; /&gt;&lt;/li&gt;
&lt;li&gt;添加引用, 在解决方案管理器中右击项目, 添加, 引用,&lt;br /&gt;
&lt;img src=&quot;/images/20200705003754911.png&quot; alt=&quot;添加引用&quot; /&gt;&lt;/li&gt;
&lt;li&gt;添加引用, 在弹出的窗口中点击浏览, 然后在选择文件窗口中选择之前生成的DLL文件, 点击添加, 最后在引用管理器中点击确定.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/20200705004309633.png&quot; alt=&quot;添加引用&quot; /&gt;
3. 于是, 我们就成功的引用了之前生成的DLL文件.
&lt;img src=&quot;/images/20200705004359951.png&quot; alt=&quot;添加引用后&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;使用DLL中包含的成员:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在创建DLL时, 可以看到, 成员均处于 CHO.Json 命名空间下, 所以我们using它, 如下图, 并没有出现错误.
&lt;img src=&quot;/images/20200705004541827.png&quot; alt=&quot;添加引用&quot; /&gt;&lt;/li&gt;
&lt;li&gt;准备一个Json文本来使程序读取.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;{
&quot;姓名&quot;: &quot;小明&quot;,
&quot;性别&quot;: &quot;男&quot;,
&quot;年龄&quot;: 16,
&quot;自述&quot;: null,
&quot;是否患新冠肺炎&quot;: false,
&quot;学习的编程语言&quot;: [&quot;C#&quot;, &quot;Python&quot;, &quot;C&quot;]
}&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;将其转换为字符串表达式.  (这里使用了我自己的小程序, 文章末尾会附上下载链接)
&lt;img src=&quot;/images/20200705005032983.png&quot; alt=&quot;转换为字符串表达式&quot; /&gt;&lt;/li&gt;
&lt;li&gt;将代码写好.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/20200705005246473.png&quot; alt=&quot;使用类库中的成员&quot; /&gt;
4. 运行程序.
&lt;img src=&quot;/images/20200705005450205.png&quot; alt=&quot;程序运行结果&quot; /&gt;
5. 可以看到, 成功使用了类库中的JsonData类,和JsonDataType枚举类型.&lt;/p&gt;
&lt;h2&gt;文章到此结束.&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;将文本转换为字符串表达式的小工具 (&lt;a href=&quot;https://chonet.lanzous.com/i5Uveebcgle&quot;&gt;蓝奏云下载&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;关于我写的这个Json操作类库(&lt;a href=&quot;/posts/20201028235244/&quot;&gt;文章&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;关于作者:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;一个喜欢编程但不喜欢在校学习奇怪东西的奇怪人士.&lt;/li&gt;
&lt;li&gt;性格沙雕, 但写文章的时候就莫名其妙的认真起来&lt;/li&gt;
&lt;li&gt;喜欢玩MC, 还有类似的沙盒游戏, 例如废品工程师&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;联系我:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;发送私信.&lt;/li&gt;
&lt;li&gt;电子邮件 : creepslime@foxmail.com&lt;/li&gt;
&lt;li&gt;私人QQ : 2056818509&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;欢迎与我交流吖   ε=ε=ε=(~ ￣▽￣)~&lt;/p&gt;
</content:encoded></item><item><title>[C#] 无需实体类, 解析读写Json, 简单操作。</title><link>https://slimenull.com/posts/20200702232419/</link><guid isPermaLink="true">https://slimenull.com/posts/20200702232419/</guid><description>1. 关于:这是我自己写的一个类库, 已经封装到了一个dll中, 暂且命名为CHO.Json, 它能够使你像Python那样操作Json数据, 非常适合新手虽然在操作较大的Json数据时, 需要实体类会更方便, 但是这个类库, 可以让你实时判断Json数据的类型. 当然, 以后也会考虑加入需实体类的序列化与反序列化.它具有较大的容差值, 所以允许一些Json不允许的操作, 例如将Bool值作为Object中的键, CHO.Json支持这样, 由CHO.Json生成的Json文本, CHO.Json完</description><pubDate>Thu, 02 Jul 2020 23:24:19 GMT</pubDate><content:encoded>&lt;h2&gt;1. 关于:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;这是我自己写的一个类库, 已经封装到了一个dll中, 暂且命名为CHO.Json, 它能够使你像Python那样操作Json数据, 非常适合新手&lt;/li&gt;
&lt;li&gt;虽然在操作较大的Json数据时, 需要实体类会更方便, 但是这个类库, 可以让你实时判断Json数据的类型. 当然, 以后也会考虑加入需实体类的序列化与反序列化.&lt;/li&gt;
&lt;li&gt;它具有较大的容差值, 所以允许一些Json不允许的操作, 例如将Bool值作为Object中的键, CHO.Json支持这样, 由CHO.Json生成的Json文本, CHO.Json完全可以读取. 因为其特殊性, 所以可能别的Json读写模块无法识别, 所以, 为了保证正常使用, 即便CHO.Json支持这些操作, 但务必遵守标准Json的规则.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;最新版的某些函数已经与介绍有些出入, 请查看新文章 &lt;a href=&quot;/posts/20201028235244/&quot;&gt;CHO.Json操作&lt;/a&gt;
已经添加了关于实体类的序列化与反序列化功能, 详见 &lt;a href=&quot;https://github.com/SlimeNull/CHO.Json&quot;&gt;Github仓库&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2. 原理:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;由一个JsonData类来表示任何Json数据, 包括Object, Array, String, ==Integer, Float, Double, Boolean==, Null, 其中由枚举属性DataType来表示该JsonData属于何种数据类型.&lt;/li&gt;
&lt;li&gt;分析Json文本, 这个就无需多言了, 如果感兴趣, 可以联系我, 获取它的源代码. 当然, 你也可以直接将dll反编译.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. 命名空间:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;该Json解析方式所需的成员位于dll中 CHO.Json 命名空间下&lt;/li&gt;
&lt;li&gt;命名空间下包含了一些异常类型: JsonDataTypeException, InvalidCharParseException, NotClosedParseException, ParseCallError, ParseUnknownError, JsonFormatParseException这些继承于Exception的类, 它们分别表示了 Json数据类型异常, 分析Json文本时的非法字符异常, 分析Json文本时的数据未结束异常, 分析Json文本时的调用错误, 分析Json文本时的未知错误(该错误在正常使用时不会出现, 除非用户自定义的类继承JsonData, 并写了一些奇怪的代码), 分析Json文本时的Json数据格式错误.&lt;/li&gt;
&lt;li&gt;命名空间下包含了枚举类型 JsonDataType, 它的成员有这些: Object, Array, String, Number, Bool, Null. 这些表示了JsonData中所包含的Json数据的类型.&lt;/li&gt;
&lt;li&gt;命名空间下包含的JsonData是CHO.Json的核心, 它提供了对所有Json数据的操作方法, 例如Array的Add方法, 提供了分析Json文本的Parse方法, 将JsonData示例转换为Json文本的ToJsonTest方法, 注意, 在使用一种操作Json数据的方法前, 请确保, 实例所包含的Json数据是方法支持的, 否则将抛出JsonDataTypeException异常.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;4. 使用:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;CHO.Json的使用基本上是对JsonData的操作, 下面给出一个实例, 以了解CHO.Json的基本使用方法.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这是一个Json文本, 接下来将使用程序来读取它并输出一些值&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;{
&quot;姓名&quot;: &quot;小明&quot;,
&quot;性别&quot;: &quot;男&quot;,
&quot;年龄&quot;: 16,
&quot;境内&quot;: true,
&quot;自述&quot;: null,
&quot;学习科目&quot;: [
&quot;语文&quot;,
&quot;数学&quot;,
&quot;英语&quot;,
&quot;信息技术&quot;
]
}&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;将其转换为字符串表达式后: ==注意, 在新版本中, 将JsonData实例转换为Json文本你需要使用JsonData的静态方法: ConvertToText==&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;{\n    &quot;姓名&quot;: &quot;小明&quot;,\n    &quot;性别&quot;: &quot;男&quot;,\n    &quot;年龄&quot;: 16,\n    &quot;境内&quot;: true,\n    &quot;自述&quot;: null,\n    &quot;学习科目&quot;: [\n         &quot;语文&quot;,\n         &quot;数学&quot;,\n         &quot;英语&quot;,\n         &quot;信息技术&quot;\n     ]\n}&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;using System;
using CHO.Json;

namespace CSharpJson
{
    class Program
    {
        static void Main(string[] args)
        {
            JsonData json = JsonData.Parse(&quot;{\n    \&quot;姓名\&quot;: \&quot;小明\&quot;,\n    \&quot;性别\&quot;: \&quot;男\&quot;,\n    \&quot;年龄\&quot;: 16,\n    \&quot;境内\&quot;: true,\n    \&quot;自述\&quot;: null,\n    \&quot;学习科目\&quot;: [\n         \&quot;语文\&quot;,\n         \&quot;数学\&quot;,\n         \&quot;英语\&quot;,\n         \&quot;信息技术\&quot;\n     ]\n}&quot;);
            if (json.DataType == JsonDataType.Object)
            {
                foreach(JsonData key in json.GetKeys())
                {
                    Console.WriteLine(string.Format(&quot;{0}: {1}&quot;, key.ToJsonText(), json[key].ToJsonText()));
                }
            }
            else
            {
                Console.WriteLine(&quot;读取出现了错误&quot;);
            }
            Console.ReadKey();
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;输出结果是:&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;姓名&quot;: &quot;小明&quot;
&quot;性别&quot;: &quot;男&quot;
&quot;年龄&quot;: 16
&quot;境内&quot;: true
&quot;自述&quot;: null
&quot;学习科目&quot;: [&quot;语文&quot;, &quot;数学&quot;, &quot;英语&quot;, &quot;信息技术&quot;]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;从结果来看, 正常分析了Json文本并且成功将Json数据转换为了Json文本.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;下载:&lt;/h2&gt;
&lt;p&gt;Github: &lt;a href=&quot;https://github.com/SlimeNull/CHO.Json&quot;&gt;Github仓库&lt;/a&gt;
.
.
.
.
.&lt;/p&gt;
&lt;h4&gt;补充:&lt;/h4&gt;
&lt;p&gt;. . .查看元数据时并不能看到方法注释, 所以, 推荐查看源代码, 那里有完整的注释.
. . .自述 : 自学C#的一只小辣鸡~ (欢迎与我交流~ []~ (￣▽￣)~*)&lt;/p&gt;
</content:encoded></item><item><title>数组去重: 超高速字符串去重 (含保留原顺序的方法)</title><link>https://slimenull.com/posts/20200627054332/</link><guid isPermaLink="true">https://slimenull.com/posts/20200627054332/</guid><description>1. 你需要知道这些代码适用于:将字符串数组中的重复元素去除, 仅留下一个应用场景:你有一个超级长的文本文件, 这里面每一行都是一条数据, 例如这些数据是用爬虫获取的搜索关键字, 但可能含有重复, 你现在需要将它们去重. 这个算法可以帮助你快速完成去重的工作局限性:下面将介绍两种算法一种是先进行排序, 这时, 相同的元素都在一起了, 然后再进行一次遍历去重, 那么除了排序的时间, 仅进行一次遍历就可以去重, 速度很快, 但原有顺序改变了.第二种算法是较为特殊的算法, 借助了引用类型</description><pubDate>Sat, 27 Jun 2020 05:43:32 GMT</pubDate><content:encoded>&lt;h2&gt;1. 你需要知道这些&lt;/h2&gt;
&lt;h5&gt;代码适用于:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;将字符串数组中的重复元素去除, 仅留下一个&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;应用场景:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;你有一个超级长的文本文件, 这里面每一行都是一条数据, 例如这些数据是用爬虫获取的搜索关键字, 但可能含有重复, 你现在需要将它们去重. 这个算法可以帮助你快速完成去重的工作&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;局限性:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;下面将介绍两种算法&lt;/li&gt;
&lt;li&gt;一种是先进行排序, 这时, 相同的元素都在一起了, 然后再进行一次遍历去重, 那么除了排序的时间, 仅进行一次遍历就可以去重, 速度很快, 但原有顺序改变了.&lt;/li&gt;
&lt;li&gt;第二种算法是较为特殊的算法, 借助了引用类型这个特性, 可以保证原有顺序不变, 但需要定义字典和列表, 也就是说要多需要一些内存.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2. 算法的主要内容&lt;/h2&gt;
&lt;h5&gt;主要原理&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;通过排序, 将相同的元素凑到一起, 那么只需要比对当前元素和一个相邻元素就可以得出该元素是否是重复的. 那么只需要遍历一次就可以做到去重.&lt;/li&gt;
&lt;li&gt;在此基础上, 用Dictionary的根据键可以快速访问值得特性, 将原字符串数组的元素索引与元素引用保存到Dictionary中, 就可以通过索引将字符串数组还原到原来的顺序, 详细还请看代码&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;实例代码&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;string[] RemoveSameElement(string[] source)
{
    List&amp;lt;string&amp;gt; result = source.ToList();
    result.Sort();
    for(int i = 1; i &amp;lt; source.Length;)
    {
        if (result[i] == result[i - 1])
        {
            result.RemoveAt(i);
        }
        else
        {
            i++;
        }
    }
    return result.ToArray();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;string[] RemoveSameElement(string[] source)
{
    Dictionary&amp;lt;int, StringInfo&amp;gt; d = new Dictionary&amp;lt;int, StringInfo&amp;gt;();
    List&amp;lt;StringInfo&amp;gt; temp = new List&amp;lt;StringInfo&amp;gt;();
    List&amp;lt;string&amp;gt; result = new List&amp;lt;string&amp;gt;();
    for (int i = 0; i &amp;lt; source.Length; i++)
    {
        d.Add(i, new StringInfo(source[i]));
        temp.Add(d[i]);
    }
    temp.Sort();
    for (int i = 0; i &amp;lt; temp.Count;)
    {
        if (temp[i] != null)
        {
            temp.RemoveAt(i);
        }
        else
        {
            i++;
        }
    }
    foreach(StringInfo i in temp)
    {
        result.Add(i.Value);
    }
    return result.ToArray();
}
// 通过这个StringInfo类来实现对string的引用
class StringInfo : IComparable
{
    public StringInfo(string value)
    {
        Value = value;
    }
    public string Value;
    public int CompareTo(object obj)
    {
        if (obj.GetType() == GetType())
        {
            return Value.CompareTo((obj as StringInfo).Value);
        }
        else
        {
            throw new ArgumentException(&quot;参数必须为StringInfo类型&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;End&lt;/h2&gt;
&lt;h6&gt;1. 这个算法只是偶然间想到的, 不喜勿喷, 也请多多指教.&lt;/h6&gt;
&lt;h6&gt;2. 算法作为一个功能嵌入到我的一个文本处理小工具中&lt;/h6&gt;
&lt;ol&gt;
&lt;li&gt;(对文本文件的每一行进行特定处理), 这个工具包含以下功能&lt;/li&gt;
&lt;li&gt;&apos;在行首行末随机插入特定的内容&apos;,&lt;/li&gt;
&lt;li&gt;&apos;在行首行末依照自己定义好的一个字符串序列,按顺序一个个插入到行首或行末&apos;,&lt;/li&gt;
&lt;li&gt;&apos;以一定初值,一定增量, 在行首或行末插入数字&apos;,&lt;/li&gt;
&lt;li&gt;&apos;根据每一行的字符串长度排序&apos;&lt;/li&gt;
&lt;li&gt;&apos;根据每一行的字符串内容排序&apos;&lt;/li&gt;
&lt;li&gt;&apos;去除功能(去除空行,去除重复行, 去除行首行末指定字符,去除包含关键字的行 等等)&apos;
.
.
.
.
.
.
.
.
.
.
.&lt;/li&gt;
&lt;/ol&gt;
&lt;h6&gt;想恰饭 (;´༎ຶД༎ຶ`)&lt;/h6&gt;
&lt;ol&gt;
&lt;li&gt;上面所说的小工具是帮人代写的, 如果你需要它并且懒得写代码, 我很乐意将他出售给你. 之前没有排序功能的时候, 原售价是50￥ (老板给了这么多)&lt;/li&gt;
&lt;li&gt;欲购买的话... 私聊就好 (虽然这的确是一个没有什么技术含量的东西)&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>排序算法: 超高速根据字符串长度排序的算法</title><link>https://slimenull.com/posts/20200626020405/</link><guid isPermaLink="true">https://slimenull.com/posts/20200626020405/</guid><description>1. 你需要知道这些:代码适用于:适用于字符串数组的元素长度变化量较小的, 比如字符串的长度普遍在1~50, 那么此时, 这个算法将有超高的性能, 测试结果是 50万条数据排序所需时间为1s多一点(你没看错).应用场景:你有一个超级长的文本文件, 这里面每一行都是一条数据, 例如这些数据是用爬虫获取的搜索关键字, 你现在需要将它们排序, 别犹豫, 我认为这个算法非常适合你.局限性字符串数组的元素长度变化量越大, 该算法效率越低, 请根据需求使用.2.算法的主要内容主要原理:定</description><pubDate>Fri, 26 Jun 2020 02:04:05 GMT</pubDate><content:encoded>&lt;h2&gt;1. 你需要知道这些:&lt;/h2&gt;
&lt;h5&gt;代码适用于:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;适用于字符串数组的元素长度变化量较小的, 比如字符串的长度普遍在1~2000, 那么此时, 这个算法将有超高的性能, 测试结果是 10万条数据排序所需时间为91ms(你没看错).&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;应用场景:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;你有一个超级长的文本文件, 这里面每一行都是一条数据, 例如这些数据是用爬虫获取的搜索关键字, 你现在需要将它们排序, 别犹豫, 我认为这个算法非常适合你.&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;局限性&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;字符串数组的元素长度变化量越大, 该算法效率可能会越低,因为里面包含了对字符串长度变化量排序的过程,这个过程是使用的选择排序法。（选择排序法的所需时间与数据长度不成正比,不多赘述）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2.算法的主要内容&lt;/h2&gt;
&lt;h5&gt;主要原理:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;定义一个字典, 键是int, 值是可变的字符串列表, 定义一个可变的字符串列表作为存储结果的容器&lt;/li&gt;
&lt;li&gt;循环源字符串数组, 将一切可能的长度作为int, 一个新的字符串列表作为值, 添加到上面所说的字典里面&lt;/li&gt;
&lt;li&gt;第二次循环源字符串数组, 将迭代到的字符串的长度作为键, 在字典中键所对应的字符串列表中添加这个字符串&lt;/li&gt;
&lt;li&gt;排序字典的键, 循环排序后的所有键, 将键对应的字符串列表的每一个字符串添加到结果中&lt;/li&gt;
&lt;li&gt;排序结束&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;示例代码:&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;// 这是C#代码
// Dictionary 与 List 位于 System.Collections.Generic 命名空间下
string[] StringSort(string[] source)
        {
            int[] sortKeys(int[] src)    // 这里是用于排序字典的键的
            {
                int most;
                int mostIndex = 0;
                int temp;
                if (src.Length &amp;gt; 1)
                {
                    for (int i = 1; i &amp;lt; src.Length; i++)
                    {
                        most = src[i];
                        for (int j = i; j &amp;lt; src.Length; j++)
                        {
                            if (src[j] &amp;lt; most)
                            {
                                most = src[j];
                                mostIndex = j;
                            }
                        }
                        if (most &amp;lt; src[i - 1])
                        {
                            src[mostIndex] = src[i - 1];
                            src[i - 1] = most;
                        }
                    }
                }
                return src;
            }

            Dictionary&amp;lt;int, List&amp;lt;string&amp;gt;&amp;gt; firstSortedString = new Dictionary&amp;lt;int, List&amp;lt;string&amp;gt;&amp;gt;();
            List&amp;lt;string&amp;gt; result = new List&amp;lt;string&amp;gt;();

            foreach (string i in source)
            {
                if (!firstSortedString.ContainsKey(i.Length))
                {
                    firstSortedString.Add(i.Length, new List&amp;lt;string&amp;gt;());
                }
            }
            foreach (string i in source)
            {
                firstSortedString[i.Length].Add(i);
            }
            foreach (int i in sortKeys(firstSortedString.Keys.ToArray()))
            {
                foreach (string j in firstSortedString[i])
                {
                    result.Add(j);
                }
            }

            return result.ToArray();
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;End&lt;/h2&gt;
&lt;h6&gt;1. 这个算法只是偶然间想到的, 不喜勿喷, 也请多多指教.&lt;/h6&gt;
&lt;h6&gt;2. 算法作为一个功能嵌入到我的一个文本处理小工具中&lt;/h6&gt;
&lt;ol&gt;
&lt;li&gt;(对文本文件的每一行进行特定处理), 这个工具包含以下功能&lt;/li&gt;
&lt;li&gt;&apos;在行首行末随机插入特定的内容&apos;,&lt;/li&gt;
&lt;li&gt;&apos;在行首行末依照自己定义好的一个字符串序列,按顺序一个个插入到行首或行末&apos;,&lt;/li&gt;
&lt;li&gt;&apos;以一定初值,一定增量, 在行首或行末插入数字&apos;,&lt;/li&gt;
&lt;li&gt;&apos;根据每一行的字符串长度排序&apos;&lt;/li&gt;
&lt;li&gt;&apos;根据每一行的字符串内容排序&apos;&lt;/li&gt;
&lt;li&gt;&apos;去除功能(去除空行,去除重复行, 去除行首行末指定字符,去除包含关键字的行 等等)&apos;
.
.
.
.
.
.
.
.
.
.
.&lt;/li&gt;
&lt;/ol&gt;
&lt;h6&gt;想恰饭 (;´༎ຶД༎ຶ`)&lt;/h6&gt;
&lt;ol&gt;
&lt;li&gt;上面所说的小工具是帮人代写的, 如果你需要它并且懒得写代码, 我很乐意将他出售给你. 之前没有排序功能的时候, 原售价是50￥ (老板给了这么多)&lt;/li&gt;
&lt;li&gt;欲购买的话... 私聊就好 (虽然这的确是一个没有什么技术含量的东西)&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>[C#] Image的Image.GetThumbnailImage(获取缩略图)方法实际是缩放与拉伸</title><link>https://slimenull.com/posts/20200526051054/</link><guid isPermaLink="true">https://slimenull.com/posts/20200526051054/</guid><description>经过测试,Image.GetThumbnailImage 方法并不只是获取缩略图,你甚至可以拿它来放大图片,以及更多骚操作稍微包装一下,就得到了下面的函数,这可真是令人愉悦呀/// &lt;summary&gt;/// 缩放图片/// &lt;/summary&gt;/// &lt;param name=&quot;source&quot;&gt;处理源&lt;/param&gt;/// &lt;param name=&quot;output&quot;&gt;输出&lt;/param&gt;/// &lt;param name=&quot;</description><pubDate>Tue, 26 May 2020 05:10:54 GMT</pubDate><content:encoded>&lt;h4&gt;经过测试,Image.GetThumbnailImage 方法并不只是获取缩略图,你甚至可以拿它来放大图片,以及更多骚操作&lt;/h4&gt;
&lt;h5&gt;稍微包装一下,就得到了下面的函数,这可真是令人愉悦呀&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// 缩放图片
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;param name=&quot;source&quot;&amp;gt;处理源&amp;lt;/param&amp;gt;
/// &amp;lt;param name=&quot;output&quot;&amp;gt;输出&amp;lt;/param&amp;gt;
/// &amp;lt;param name=&quot;times&quot;&amp;gt;倍数&amp;lt;/param&amp;gt;
/// &amp;lt;returns&amp;gt;表示操作是否成功&amp;lt;/returns&amp;gt;
private bool GetMicroImage(Image source, out Image output, int times)
{
    try
    {
        output = source.GetThumbnailImage(source.Width * times, source.Height * times , () =&amp;gt; false, IntPtr.Zero);
        return true;
    }
    catch
    {
        output = source;
        return false;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;需要注意的是(GetThumbnailImage方法):&lt;/h4&gt;
&lt;h5&gt;1.缩放时,如果指定的宽高比例与原图宽高比例不同,则输出的图片较原图还进行了拉伸&lt;/h5&gt;
&lt;p&gt;也就是说,GetThumbnailImage 方法对原图处理时,并不是等比例缩放&lt;/p&gt;
</content:encoded></item><item><title>[C#] 简易的聊天气泡(很简单的实现)</title><link>https://slimenull.com/posts/20200422081050/</link><guid isPermaLink="true">https://slimenull.com/posts/20200422081050/</guid><description>效果图能满足我自己的需求了直接看代码吧真的很简单…这是一个类,直接复制粘贴过去就好,不需要什么引用class ChatBubble        {            public ChatBubble(Panel panel, Font font)            {                if (panel.Controls.Count != 0) thr...</description><pubDate>Wed, 22 Apr 2020 08:10:50 GMT</pubDate><content:encoded>&lt;h2&gt;效果图&lt;/h2&gt;
&lt;h5&gt;能满足我自己的需求了&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;/images/20200422080806892.png&quot; alt=&quot;效果预览&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;直接看代码吧&lt;/h2&gt;
&lt;h5&gt;真的很简单...&lt;/h5&gt;
&lt;p&gt;这是一个类,直接复制粘贴过去就好,不需要什么引用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ChatBubble
        {
            public ChatBubble(Panel panel, Font font)
            {
                if (panel.Controls.Count != 0) throw new Exception(&quot;指定Panel控件不为空!&quot;);
                ChatPlace = panel;
                BubbleFont = font;
                MsgMaxLength = panel.Width - (6 * 4 + 35 * 2);                       // 其中, 四个6为 图片与(容器以及消息文本框)的距离 ,两个35为两侧图片的大小.
            }

            readonly Panel ChatPlace;
            readonly Font BubbleFont;

            int NowY = 7;

            readonly int MsgMaxLength;

            public enum MsgPlace
            {
                Left,
                Right
            }              // 气泡创建的位置

            public void AddMsg(Image Photo, string Text, MsgPlace Place, string Name)
            {
                if (ChatPlace.Controls.Count != 0) NowY = ChatPlace.Controls[ChatPlace.Controls.Count - 2].Location.Y + ChatPlace.Controls[ChatPlace.Controls.Count - 1].Height + 7;

                PictureBox photo = new PictureBox();                // 头像
                Label nickname = new Label();                       // 昵称
                Label msg = new Label();                            // 消息内容

                photo.Size = new Size(35, 35);
                photo.SizeMode = PictureBoxSizeMode.StretchImage;
                photo.Image = Photo;

                nickname.AutoSize = true;
                nickname.MaximumSize = new Size(0, 0);
                nickname.Font = BubbleFont;
                nickname.Text = Name;

                msg.AutoSize = true;
                msg.Font = BubbleFont;
                msg.MaximumSize = new Size(MsgMaxLength, 0);
                msg.Text = Text;
                msg.BorderStyle = BorderStyle.FixedSingle;

                ChatPlace.Controls.Add(nickname);
                ChatPlace.Controls.Add(photo);
                ChatPlace.Controls.Add(msg);

                if (Place == MsgPlace.Left)
                {
                    photo.Location = new Point(7, NowY);
                    nickname.Location = new Point(photo.Location.X + photo.Width + 6, NowY);
                    msg.Location = new Point(nickname.Location.X, NowY + nickname.Size.Height);
                }
                else
                {
                    photo.Location = new Point(ChatPlace.Width - 7 - 35 - 17, NowY);
                    nickname.Location = new Point(photo.Location.X - 7 - nickname.Width - 17, NowY);
                    msg.Location = new Point(photo.Location.X - 7 - msg.Width - 17, NowY + nickname.Size.Height);
                    // 这里的减去17是除去滚动条的宽度
                }
            }
        }        // 简易聊天气泡
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用这个类时,是需要提供一个Panel控件对象作为气泡创建的位置.&lt;/p&gt;
&lt;p&gt;可以添加两种消息,左侧或者右侧.&lt;/p&gt;
&lt;p&gt;最重要的原理就是Label的AutoSize,通过这个,我们就可以省去很多麻烦,然后再计算一个气泡最大的宽度可以是多少,设置Label的最大宽度,于是又实现了自动换行.&lt;/p&gt;
&lt;h5&gt;使用方式:&lt;/h5&gt;
&lt;p&gt;在自己的项目中添加这个类,之后实例化
实例化时需要提供一个panel控件实例来作为气泡创建的容器
之后要添加消息的时候直接使用实例的方法 AddMsg即可,参数中已经包含了用户头像(Photo),消息内容(Text),消息位置(Place),发消息的用户(Name).&lt;/p&gt;
&lt;h2&gt;你可能也需要这个&lt;/h2&gt;
&lt;h5&gt;. 不一定与上面的代码有关,但与聊天气泡有关&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;TestBox textBox1 = new TextBox();
int Lines = textBox1.GetLineFromCharIndex(textBox1.Text.Length) - 1;
//Lines即为textBox1中文本的行数(包括自动换行)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>C# 从网络上下载文件</title><link>https://slimenull.com/posts/20200416032354/</link><guid isPermaLink="true">https://slimenull.com/posts/20200416032354/</guid><description>之前一直不理解如何是从网络上下载文件的…自己试了试懂了FileStream file = File.CreatWrite(filePath);                          // 创建文件WebRequest.Creat(url).GetResponse().GetResponseStream().CopyTo(file); // 创建WebRequest对象,获取响应,...</description><pubDate>Thu, 16 Apr 2020 03:23:54 GMT</pubDate><content:encoded>&lt;p&gt;之前一直不理解如何是从网络上下载文件的...自己试了试懂了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FileStream file = File.CreatWrite(filePath);                          // 创建文件

WebRequest.Creat(url).GetResponse().GetResponseStream().CopyTo(file); // 创建WebRequest对象,获取响应,获取响应流,将响应流写入到文件流

file.Close();                                                         //关闭文件

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;嗯,这种简洁的代码最喜欢了 (虽然简洁...但是没人愿意看的hhh)&lt;/p&gt;
&lt;p&gt;或者也可以写成函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool DownloadFile(string url, string toDirectory, string fileName, int timeout = 2000)    //返回值是是否下载成功
{
    string PathCombine(string path1,string path2)    //合成路径
    {
        if (path1.EndsWith(&quot;\\&quot;))
        {
            path1 = path1.Substring(0, path1.Length - 1);
        }
        if (path2.StartsWith(&quot;\\&quot;))
        {
            path2 = path2.Substring(1);
        }
        return path1 + &quot;\\&quot; + path2;
    }
        
    if(!Directory.Exists(toDirectory))
    {
        Directory.CreatDirectory(toDirectory);
    }
    
    FileStream file = File.OpenWrite(fileName);
    WebRequest request = WebRequest.Creat(url);
    request.Timeout = timeout; //设置请求超时为函数参数
    
    try
    {
        request.GetResponse().GetResponseStream().CopyTo(file);
    }
    catch
    {
        return false;
    }
    
    file.Close();
    return true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;嗯,这样好了一些,因为设置了请求超时&lt;/p&gt;
</content:encoded></item><item><title>C# 动态输入</title><link>https://slimenull.com/posts/20200406034849/</link><guid isPermaLink="true">https://slimenull.com/posts/20200406034849/</guid><description>C# 动态输入,在输入时你也可以访问你写入的内容1.缘由1.缘由. 最开始是我在写一个网络聊天程序(其实简陋的要命),然后服务端懒得写界面,就直接用的控制台,然后又想实现一些小指令,比如禁言,踢人,禁IP什么的,但是服务端在接收消息后就会直接将消息信息打印在控制台上例如这样 (下划线是光标所在处)老王 : 哎,房租又涨了,又得吃土了老张 : 啧啧啧,又在炫富了,我连土都莫得吃_但...</description><pubDate>Mon, 06 Apr 2020 03:48:49 GMT</pubDate><content:encoded>&lt;h3&gt;C# 动态输入,在输入时你也可以访问你写入的内容)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，这里是在我刚学C#时写的，但我不想删除任何我的足迹.
==这个类库不是完善的，如果需要完整功能(真的很好用)，请去我的新文章==: &lt;a href=&quot;/posts/20201230223307/&quot;&gt;[C#] 控制台动态输入 - 增强版ReadLine()&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;代码:&lt;/h2&gt;
&lt;p&gt;主要就是能够实现在输入的同时,子线程可以通过该实例的Text属性来访问已经输入了的内容,别的倒也懒得实现了awa&lt;/p&gt;
&lt;p&gt;刚学不久,不喜勿喷&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//其实代码不怎么好,做做参考就可以了,注意:没有普通Console.ReadLine()的上下键功能

    class DynamicInput
    {
        class CharInfo
        {
            public CharInfo(int position,char chr)
            {
                this.position = position;
                this.chr = chr;
            }
            int position;
            char chr;
            public int Position
            {
                get
                {
                    return position;
                }
            }
            public char Char
            {
                get
                {
                    return chr;
                }
            }
        }

        delegate void Add_historyEventHandler(object sender,EventArgs e);

        private int default_left;
        private string text = &quot;&quot;;
        private List&amp;lt;string&amp;gt; input_history = new List&amp;lt;string&amp;gt;();
        private List&amp;lt;CharInfo&amp;gt; input_list = new List&amp;lt;CharInfo&amp;gt;();

        public string Text
        {
            get
            {
                return text;
            }
        }
        public string Start()
        {
            default_left = Console.CursorLeft;
            ConsoleKeyInfo key;
            while (true)
            {
                key = Console.ReadKey();
                if (key.Key.Equals(ConsoleKey.Enter))
                {
                    return text;
                }        //回车确认
                if (key.Key.Equals(ConsoleKey.Backspace))
                {
                    Console.Write(&quot; &quot;);
                    if (Console.CursorLeft &amp;gt; input_list.Count)
                    {
                        if (input_list.Count &amp;gt;1 )
                        {
                            for (int i = 0; i &amp;lt;= Console.CursorLeft - input_list[input_list.Count-2].Position; i ++)
                            {
                                Console.Write(&quot;\b \b&quot;);
                            }
                        }
                        else
                        {
                            for (int i = 0; i &amp;lt;= Console.CursorLeft - default_left; i++)
                            {
                                Console.Write(&quot;\b \b&quot;);
                            }
                        }
                    }
                    else
                    {
                        if (input_list.Count &amp;gt; 1)
                        {
                            for (int i = 0; i &amp;lt;= Console.CursorLeft + Console.WindowLeft - input_list[input_list.Count - 2].Position; i++)
                            {
                                Console.Write(&quot;\b \b&quot;);
                            }
                        }
                        else
                        {
                            for (int i = 0; i &amp;lt;= Console.CursorLeft + Console.WindowLeft - default_left; i++)
                            {
                                Console.Write(&quot;\b \b&quot;);
                            }
                        }
                    }
                    if(input_list.Count &amp;gt; 0)
                    {
                        text = text.Substring(0, text.Length - 1);
                        input_list.RemoveAt(input_list.Count - 1);
                    }
                    continue;
                }    //BackSpace退格
                if (!key.Key.Equals(ConsoleKey.Spacebar) &amp;amp; (key.KeyChar == &apos; &apos;|key.KeyChar == &apos;\0&apos;))
                {
                    Console.Write(&quot;\b&quot;);
                    continue;
                }
                text += key.KeyChar;                            //更新Text
                input_list.Add(new CharInfo(Console.CursorLeft, key.KeyChar));   //记录字符与位置
            }
        }
    }
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>