2024年3月

本次分享一下如何将GPT-SoVITS接入SillyTavern-1.11.5项目,让让AI女友声若幽兰,首先明确一下,SillyTavern-1.11.5只是一个前端项目,它没有任何大模型文本生成能力,所以后端必须有一个api服务来流式生成对话文本,这里选择koboldcpp。

首先看一下简单的项目运行架构图:

这里SillyTavern作为前端负责向后端的Koboldcpp发起请求,Koboldcpp流式返回文本,SillyTavern接受聊天文本进行展示,当文本接受完毕后,SillyTavern再次向后端的GPT-SoVITS发起请求,将全量文本传递给后端GPT-SoVITS,GPT-SoVITS根据文字来生成语音,并将语音的二进制文件返回给SillyTavern,最后SillyTavern播放音频,至此,一个完整的流程就走完了。

部署SillyTavern

首先克隆SillyTavern的官方项目:

git clone https://github.com/SillyTavern/SillyTavern.git

直接运行启动脚本即可:

shell start.sh

如果是windows平台,运行bat:

start.bat

由于SillyTavern没有预留GPT-SoVITS的位置,所有将原本的XTTS改为GPT-SoVITS:

async fetchTtsGeneration(inputText, voiceId) {  
        console.info(`Generating new TTS for voice_id ${voiceId}`);  
  
        if (this.settings.streaming) {  
            const params = new URLSearchParams();  
            params.append('text', inputText);  
            params.append('speaker_wav', voiceId);  
            params.append('language', this.settings.language);  
            return `${this.settings.provider_endpoint}/tts_stream/?${params.toString()}`;  
        }  
  
        const response = await doExtrasFetch(  
            `${this.settings.provider_endpoint}/tts_to_audio/`,  
            {  
                method: 'POST',  
                headers: {  
                    'Content-Type': 'application/json',  
                    'Cache-Control': 'no-cache',  // Added this line to disable caching of file so new files are always played - Rolyat 7/7/23  
                },  
                body: JSON.stringify({  
                    'text': inputText,  
                    'speaker_wav': voiceId,  
                    'language': this.settings.language,  
                }),  
            },  
        );  
        if (!response.ok) {  
            toastr.error(response.statusText, 'TTS Generation Failed');  
            throw new Error(`HTTP ${response.status}: ${await response.text()}`);  
        }  
        return response;  
    }

部署Koboldcpp

随后部署后端的大模型api:

git clone https://github.com/LostRuins/koboldcpp.git

输入编译命令

windows平台:

make

Mac平台:

make LLAMA_METAL=1

安装依赖:

pip install -r requirements.txt

启动服务:

Python3 koboldcpp.py --model /Users/liuyue/Downloads/causallm_7b-dpo-alpha.Q5_K_M.gguf  --gpulayers 40 --highpriority --threads 300

此时接口运行在http://localhost:5001

部署GPT-SoVITS

最后,部署GPT-SoVITS项目:

git clone https://github.com/RVC-Boss/GPT-SoVITS.git

安装依赖:

pip3 install -r requirements.txt

修改一下api接口逻辑:

@app.post("/")  
async def tts_endpoint(request: Request):  
    json_post_raw = await request.json()  
    return handle(  
        json_post_raw.get("refer_wav_path"),  
        json_post_raw.get("prompt_text"),  
        json_post_raw.get("prompt_language"),  
        json_post_raw.get("text"),  
        json_post_raw.get("text_language"),  
        json_post_raw.get("sweight"),  
        json_post_raw.get("gweight"),  
    )  
  
  
@app.get("/")  
async def tts_endpoint(  
        refer_wav_path: str = None,  
        prompt_text: str = None,  
        prompt_language: str = None,  
        text: str = None,  
        text_language: str = None,  
        sweight: str = None,  
        gweight: str = None,  
):  
    return handle(refer_wav_path, prompt_text, prompt_language, text, text_language,sweight,gweight)  
  
  
def speaker_handle():  
  
    return JSONResponse(["female_calm","female","male"], status_code=200)  
  
  
@app.get("/speakers_list")  
async def speakerlist_endpoint():  
    return speaker_handle()  
  
  
def tts_to_audio_handle(text):  
  
    return handle(llama_audio,llama_text,llama_lang,text,"中英混合")  
  
  
@app.post("/tts_to_audio/")  
async def tts_to_audio(request: Request):  
    json_post_raw = await request.json()  
    return tts_to_audio_handle(json_post_raw.get("text"))  
  
  
if __name__ == "__main__":  
    uvicorn.run(app, host="0.0.0.0", port=port, workers=1)

这里添加新的基于get方法的speakers_list,是为了配合xtts接口的格式,同时基于post方法的tts_to_audio方法用来生成语音,它只接受一个参数text,也就是需要转为语音的文本。

至此,三个服务就都配置好了,最后奉上视频教程:

https://www.bilibili.com/video/BV1uJ4m1a7L4/

前言:

当我们涉及到在运行时生成和定义方法时,便需要使用到C#中的两个关键类之一:MethodBuilder 或 DynamicMethod。

这两者都属于反射(Reflection.Emit)的一部分,允许我们以动态的方式创建方法。

两者各有侧重,使用方式大体相同,本篇文章我们先介绍 MethodBuilder,再介绍 DynamicMethod,最后再总结两者的区别。

1、MethodBuilder 介绍:

MethodBuilder
是一个强大的工具,用于在动态程序集中创建方法。

如果你需要构建整个类型(包括字段、属性、方法等),那么按流程:

首先你需要创建一个动态程序集(
AssemblyBuilder
),

然后在其中创建一个模块(
ModuleBuilder
),

最后再创建一个或多个类型(
TypeBuilder
)。

而要在这些类型中创建方法,就可以使用
MethodBuilder

其关键特点:

  • 绑定到类型

    MethodBuilder
    创建的方法是属于某个类型的一部分,因此只能通过该类型的实例或静态引用来调用。
  • 方法签名
    :需要指定方法名称、参数类型和返回类型。
  • IL代码生成
    :需要手动编写IL代码。

2、MethodBuilder 代码:定义方法

正如上文所说,MethodBuilder 的使用,需要定义整个程序集上下文。

因此我们先编写一下共用部分代码,同样用于( .NET 版本生成程序集,以便对照生成代码):

AssemblyName assName = new AssemblyName("myAssembly") { Version = new Version("1.1.1.2") };
AssemblyBuilder ab =AppDomain.CurrentDomain.DefineDynamicAssembly(assName, AssemblyBuilderAccess.RunAndSave);
ModuleBuilder mb = ab.DefineDynamicModule("myModule", "b.dll");
TypeBuilder tb = mb.DefineType("MyNameSpace.MyClass", TypeAttributes.Public |TypeAttributes.Class);


//定义方法......
tb.CreateType();
ab.Save("b.dll");

方法定义的过程:

1、通过 TypeBuilder 的 DefineMethod 来定义方法:MethodBuilder methodBuilder = tb.DefineMethod("方法名",......);2、通过构造函数,可以设定方法定义的参数;3、也可以后面再通过 MethodBuilder 实例 的 SetXXX 及  系列来定义参数。

下面示例展示方法的定义:

A、定义实例方法:使用简单参数

 //定义实例方法:通过构造函数指定方法修饰符:Public、返回值:typeof(string)、参数类型:typeof(int),typeof(string)
 MethodBuilder methodBuilder = tb.DefineMethod("MyMethod", MethodAttributes.Public, typeof(string), new Type[] { typeof(int), typeof(string) });
//定义参数的名称:指定参数名称:id,name
methodBuilder.DefineParameter(
1, ParameterAttributes.None, "id");
methodBuilder.DefineParameter(
2, ParameterAttributes.None, "name");
//用IL编写方法实现
var il =methodBuilder.GetILGenerator();
il.EmitWriteLine(
"hello world!");
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ret);
对照生成的代码:

注意事项:

1:DefineParameter 对参数的索引从1开始(对实例方法或静态方法都一样)。
2:IL 构建代码时:实例方法下 Ldarg_0 是 this 自身,静态方法下 Ldarg_0 是id参数。

B、定义静态方法:参数进阶【包括ref、out、指针】大杂绘

//定义静态方法
MethodBuilder methodBuilder = tb.DefineMethod("MyMethod", MethodAttributes.Public | MethodAttributes.Static, typeof(object), new Type[] { typeof(int).MakeByRefType(), typeof(string).MakePointerType() });
methodBuilder.DefineParameter(
1, ParameterAttributes.None, "id");
methodBuilder.DefineParameter(
2, ParameterAttributes.Out | ParameterAttributes.Optional, "name");var il =methodBuilder.GetILGenerator();
il.EmitWriteLine(
"hello world!");
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ret);

对照生成的代码:

简要说明:

1、ref 参数定义:在构造函数类型用:typeof(int).MakeByRefType()
2、指针 参数定义:在构造函数类型用:typeof(int).MakePointerType()
3、out参数定义:在 DefineParameter 方法的 ParameterAttributes.Out 参数指定。
注意事项:在静态方法中,IL 代码 Ldarg_0 指向第一个参数 id。

C、定义方法:添加特性

 //添加自定义特性
 ConstructorInfo attributeConstructor = typeof(AssemblyTitleAttribute).GetConstructor(new Type[] { typeof(string) });
CustomAttributeBuilder attributeBuilder
= new CustomAttributeBuilder(attributeConstructor, new object[] { "ExampleAttribute"});
methodBuilder.SetCustomAttribute(attributeBuilder);

对照生成的代码:

3、MethodBuilder 代码:方法的动态调用

对于运行时生成的动态方法实现动态调用,可以通过反射调用。

下面演示(将方法变更为实例方法,使用反射调用):

通过获取类的定义Type classType,然后Type.GetMethod获取方法进行调用,并传进创建的实例作为参数之一。

当然,如果方法调用频率很高,要更进一步,也可以为 MethodInfo 创建委托,来实现更高效率的调用。

然而:

Delegate.CreateDelegate(......) 只支持从静态方法转委托方法,不支持实例方法。 

所以:

默认是不能实现对实例 MethodInfo 转委托的高效调用的。

但,那是默认,经过本人的百日创新,结合 DynamicMethod,还是有办法实现的。

实现方式可以参考本人的开源框架
Taurus.MVC
中关于 Deletgate 的相关实现。

既然提到了 DynamicMethod,那下面就开始介绍它了。

4、DynamicMethod 介绍:

DynamicMethod
则更加灵活。它允许在运行时生成和执行方法,而无需创建动态程序集或动态类型来容纳该方法。

这意味着你可以直接生成和执行少量代码,而不必担心整个类型的构建。

关键特点:

  • 不绑定到类型

    DynamicMethod
    不属于特定的类型,因此可以在任何上下文中调用。
  • 性能
    :通常比
    MethodBuilder
    更高效。

5、DynamicMethod 代码:

 //创建动态方法
 DynamicMethod dynamicMethod = new DynamicMethod("MyMethod", typeof(void), null);var il =dynamicMethod.GetILGenerator();
il.EmitWriteLine(
"hello world!");
il.Emit(OpCodes.Ret);
//创建调用委托 var deletegateMethod = dynamicMethod.CreateDelegate(typeof(Action)) asAction;//执行委托 deletegateMethod();

执行结果:

简要说明:

1、DynamicMethod 拥有的基础定义方法和 MethodBuilder 基本一致。2、DynamicMethod 动态方法关注点是运行时执行,因此,可以省略很多参数的定义,只需要定义最简单的方法名、返回值、输入参数。3、DynamicMethod 默认创建的静态方法,因此,它可以默认拥有 CreateDelegate 来实现高效调用。

6、DynamicMethod 与 MethodBuilder 两者的区别:

在C#中,
MethodBuilder

DynamicMethod
都属于反射发射(Reflection.Emit)的一部分,用于在运行时生成和定义方法。

让我们来简要介绍一下它们之间的区别:

  1. DynamicMethod:


    • DynamicMethod
      类允许在运行时生成和执行方法,而无需创建动态程序集或动态类型来容纳该方法。
    • 由即时(JIT)编译器生成的可执行代码在
      DynamicMethod
      对象被回收时也会被回收。
    • 动态方法是生成和执行少量代码的最有效方式。
    • 如果需要动态创建一个或多个方法,应使用
      DynamicMethod
  2. MethodBuilder:


    • MethodBuilder
      用于在动态程序集中创建方法。
    • 如果要创建整个类型(包括字段、属性、方法等),则需要先创建一个动态程序集(
      AssemblyBuilder
      ),然后在其中创建一个模块(
      ModuleBuilder
      ),最后再创建一个或多个类型(
      TypeBuilder
      )。
    • 若要在这些类型中创建方法,可以使用
      MethodBuilder

通俗的讲人话即是:

1、如果是生成动态程序集,包括创建动态类,那么使用 MethodBuilder。2、如果只是定义动态方法供调用,使用 DynamicMethod,因为它不用定义整个程序集,直接起手就是方法。3、使用委托调用方法:MethodBuilder 和 DynamicMethod 都支持,但 DynamicMethod 直接提供 CreateDelegate 方法,方便起手调用。

总结:

在本文的第五部分中,我们深入探讨了 .NET Emit 中动态生成方法的两种方式:MethodBuilder 和 DynamicMethod。

通过 MethodBuilder,我们可以在运行时动态创建和定义方法,为其添加参数、自定义属性等元数据信息。

而 DynamicMethod 则提供了一种更灵活的动态方法生成方式,特别适合于需要高性能的动态代码生成场景。

在本篇教程中,我们学习了如何使用 MethodBuilder 来创建动态方法,并且演示了如何定义带有引用参数的动态方法以及如何向动态方法添加自定义属性。

这些内容对于理解动态方法的创建和扩展具有重要意义。

在下一篇中,我们将重点讲述IL语言,IL(Intermediate Language)是.NET平台上的一种中间语言,它是由C#、VB.NET等高级语言编译成的一种低级语言表示形式。

我们将会详细介绍IL语言的基本结构、指令集、堆栈操作等内容,帮助读者更深入地理解.NET动态方法的内部实现和执行过程。

通过深入了解IL语言,读者将能够更好地掌握.NET平台上动态代码生成的技术,并且能够对IL代码进行优化和调试,从而更好地应用于实际的软件开发项目中。

敬请期待下一篇教程的发布,让我们一起探究IL语言的奥秘吧!

本插件可以使用蓝图创建WebSocket服务器,并监听响应数据。

下载地址在文章最后。

1. 节点说明

Create Web Socket Server – 创建WebSocket服务器对象并开启监听

创建一个WebSocket服务器对象,并监听相应端口,连接地址为 ws://IP:PORT, 比如ws://192.168.1.5:9001

返回的对象需要提升为变量,以后就是用这个对象去操作。


Bind – 绑定回调函数

绑定服务器回调函数

User Open :用户连接的时候回调。

User Close :用户断开连接的时候回调。

User Message :用户发送的消息

User ID :用户的唯一标识,可以在用户连接的时候保存一下,用于以后给用户发送消息使用。

Data :用户的连接消息,可以解析用户的 Protocols,Heads,Params。

Message :用户发送的消息。


Close Server – 关闭服务器

关闭当前服务器,关闭后可以把相应对象设置为空。

如果需要再次使用,需要重新创建服务器。


Get All Users – 获取所有玩家

获取当前所有连接用户ID。


Get User Data – 获取用户数据

获取指定用户的连接信息。


Send Message – 发送消息

给指定用户发送字符串数据。

User ID : 用户的唯一标识,可以在用户连接的时候获取到。

Message :需要发送的字符串,可以为Json字符串。


Close User – 主动关闭用户

主动断开指定用户连接。


2. 使用案例

插件中 BP_DTWebSocketServerActor 直接拖进去主场景即可看到。


3. 插件下载

【虚幻引擎】DTWebSocketServer 蓝图创建WebSocket服务器插件使用说明 – DT

前言

相信很多做WPF开发的小伙伴都遇到过表格类的需求,虽然现有的Grid控件也能实现,但是使用起来的体验感并不好,比如要实现一个Excel中的表格效果,估计你能想到的第一个方法就是套Border控件,用这种方法你需要控制每个Border的边框,并且在一堆Bordr中找到Grid.Row,Grid.Column来确定位置,明明很简单的一个功能,硬是耗费了大量时间。Grid的这种设计虽然功能很强大,但是同时也导致了操作繁琐可读性非常差的问题。此时做过web开发的人肯定很想念html中的table元素,没错,我也是这样想的,如果能把html中的table元素搬到WPF中,那问题就轻松解决了,今天我们就来解决这个问题。

一、
准备工作

我们先来认识一下table元素,其实最开始的网页功能相对简单,table元素主要用于展示文本和基本的排版。然而随着html标准的更新,table元素越来越复杂,很多功能在不同的标准中写法可能不一样,甚至有的功能只能在css中实现,这种情况我们成全照搬html中的写法肯定不现实,也完全没必要。所以必须做一个取舍。
由于WPF中
并没有css的概念,所以我们尽量舍弃css中的写法,使用WPF中类似的属性写法来开发,以下为统计出来的可用属性。

二、
需求分析

既然我们要复刻一个东西,第一步肯定是要先搞清楚这个东西的内在逻辑,所以我们先来看看html中的table元素是怎么回事。

2.1 table结构

<table>
  <tr>
    <th>header1</th>
    <th>header2</th>
    <th>header3</th>
  </tr>
  <tr>
    <td>value1</td>
    <td>value2</td>
    <td>value3</td>
  </tr>
  <tr>
    <td>value4</td>
    <td>value5</td>
    <td>value6</td>
  </tr>
</table>

2.1.1 table

table为表格根元素,table内可以放置多个tr。

2.1.2 tr

tr表示表格中的一行,一行可以放置若干个td。

2.1.3 td

td为表格单元格,td可以设置rowspan属性合并多个行,可以设置colspan合并多个列。

2.2
尺寸
单位

2.2.1适用范围

table的width,height属性,tr的height属性,td的width,heigth属性。

2.2.2 取值范围

    1. 百度比(例:width="50%")

    2. 像素(

      :width="500")

    3. 不设置(
      自动计算)

2.3 布局逻辑

2.3.1 table


2.3.1.1 width="50%"

宽度占可用空间的50%,当父控件尺寸改变时会重新计算宽度,如果所有td子元素的尺寸之合大于table宽度(
width="50%"
),table宽度==Sum(td.width)。

2.3.1.2 width="500"

宽度占500像素,
当父控件尺寸改变时不会重新计算宽度,
如果所有td子元素的尺寸之合大于table宽度(
width="500"
),table宽度==Sum(td.width)。

2.3.1.3 不设置宽度

不设置宽度的情况下,宽度根据td子元素的宽度计算,
Sum(td.width)


2.3.2 tr

2.3.2.1 height="50%"

高度占table元素总高的50%,
当父控件尺寸改变时会重新计算高度,
当tr中高度最高的td超过了tr的50%时,整行高度以该td的高度为准。

2.3.2.2 height="500"

高度占500像素,
当父控件尺寸改变时会重新计算高度,当tr中高度最高的td超过了tr的500像素时,整行高度以该td的高度为准。

2.3.2.3 不设置高度

不设置高度的情况下,以最高的td子元素为准。

2.3.3 td

2.3.3.1 width="50%"

宽度占table宽度的50%,当剩余宽度不足以分配给其它列时会压缩该列的50%宽度,分配给其它列。该列的实际宽度以该列所有td的最大宽度为准。

2.3.3.2 width="50"

宽度占50像素,
当剩余宽度不足以分配给其它列时会压缩该列的50像素宽度,分配给其它列。
该列的实际宽度以该列所有td的最大宽度为准。


2.3.3.3 不设置宽度

不设置宽度的情况下,如果其它设置了宽度的列分配完宽度后,剩余宽度大于所有td的最小宽度的总合,那么未设置宽度的列会平均分配剩余的宽度,如果剩余的宽度小于所有td最小宽度的总合,那么所有td的宽度按最小宽度分配,其它已设置宽度的列则压缩宽度。
该列的实际宽度以该列所有td的最大宽度为准。

2.3.3.4 height="50%"

高度占table

度的50%,当剩余

度不足以分配给其它

时会压缩该

的50%

度,分配给其它行。该行的实际

度以该行所有td的最大

度为准。如果最高td的高度大于tr,则以最高的td为准,如果小于tr,则以tr的高度为准。

2.3.3.5 height="50"

高度占50像素,当剩余

度不足以分配给其它行时会压缩该行的50像素

度,分配给其它行。
该行的实际

度以该行所有td的最大

度为准。

如果最高td的高度大于tr,则以最高的td为准,如果小于tr,则以tr的高度为准。

2.3.3.6 不设置高度

不设置高度的情况下,如果其它设置了

度的行分配完

度后,剩余

度大于所有td的最小

度的总


,那么未设置

度的行会平均分配剩余的

度,如果剩余的

度小于所有td最小

度的总合,那么所有td的

度按最小

度分配,其它已设置

度的行则压缩

度。该行的实际

度以该列所有td的最大

度为准。

三、

能实现

通过对需求的分析,我们知道至少应该有3个类来实现表格功能,分别是Table、Tr、Td,我们下面来看看怎么来实现它们。

3.1 Table控件

Table是一个在界面上需要呈现的元素,该控件主要处理布局及排列,不需要控件模板,所以不应该继承自Control类,那么可不可以继承自Panel呢,明显也不行,Panel的尺寸及布局系统继承自FrameworkElement,并不能给它的宽度设置Width="50%"这种值,所以它不仅不能继承自Panel,也不能继承自FrameworkElement,所以Table应该继承自UIElement类,我们需要在Table写自己的尺寸及布局管理功能,以下为Talbe的示例代码。

[ContentProperty("Rows")]public classTable : UIElement
{
/// <summary> ///获取或设置行/// </summary> publicTrCollection Rows
{
get { return(TrCollection)GetValue(RowsProperty); }private set{ SetValue(RowsProperty, value); }
}
public static readonly DependencyProperty RowsProperty =DependencyProperty.Register("Rows", typeof(TrCollection), typeof(Table));/// <summary> ///获取或设置宽度/// </summary> publicTableLength Width
{
get { return(TableLength)GetValue(WidthProperty); }set{ SetValue(WidthProperty, value); }
}
public static readonly DependencyProperty WidthProperty =DependencyProperty.Register("Width", typeof(TableLength), typeof(Table));/// <summary> ///获取或设置高度/// </summary> publicTableLength Height
{
get { return(TableLength)GetValue(HeightProperty); }set{ SetValue(HeightProperty, value); }
}
public static readonly DependencyProperty HeightProperty =DependencyProperty.Register("Height", typeof(TableLength), typeof(Table));
}

3.2 Tr

Tr在Table里主要的作用是表达逻辑关系,不需要在界面上呈现,所以我们可以让它继承自DependencyObject,可以绑定属性就行了,以下为示例代码。

[ContentProperty("Cells")]public classTr : DependencyObject
{
/// <summary> ///获取或设置单元格/// </summary> publicTdCollection Cells
{
get { return(TdCollection)GetValue(CellsProperty); }private set{ SetValue(CellsProperty, value); }
}
public static readonly DependencyProperty CellsProperty =DependencyProperty.Register("Cells", typeof(TdCollection), typeof(Tr));/// <summary> ///获取或设置高度/// </summary> publicTableLength Height
{
get { return(TableLength)GetValue(HeightProperty); }set{ SetValue(HeightProperty, value); }
}
public static readonly DependencyProperty HeightProperty =DependencyProperty.Register("Height", typeof(TableLength), typeof(Tr));
}

3.3 Td

Td的情况与Table类似,需要在界面上呈现,并且有自己的尺寸及布局逻辑,所以继承自UIElement,以下为示例代码。

public classTd : UIElement
{
/// <summary> ///获取或设置需要跨的列数/// </summary> public intColSpan
{
get { return (int)GetValue(ColSpanProperty); }set{ SetValue(ColSpanProperty, value); }
}
public static readonly DependencyProperty ColSpanProperty =DependencyProperty.Register("ColSpan", typeof(int), typeof(Td), new PropertyMetadata(1));/// <summary> ///获取或设置需要跨的行数/// </summary> public intRowSpan
{
get { return (int)GetValue(RowSpanProperty); }set{ SetValue(RowSpanProperty, value); }
}
public static readonly DependencyProperty RowSpanProperty =DependencyProperty.Register("RowSpan", typeof(int), typeof(Td), new PropertyMetadata(1));/// <summary> ///获取或设置宽度/// </summary> publicTableLength Width
{
get { return(TableLength)GetValue(WidthProperty); }set{ SetValue(WidthProperty, value); }
}
public static readonly DependencyProperty WidthProperty =DependencyProperty.Register("Width", typeof(TableLength), typeof(Table));/// <summary> ///获取或设置高度/// </summary> publicTableLength Height
{
get { return(TableLength)GetValue(HeightProperty); }set{ SetValue(HeightProperty, value); }
}
public static readonly DependencyProperty HeightProperty =DependencyProperty.Register("Height", typeof(TableLength), typeof(Table));
}

3.4.1
MeasureCore()

该方法传入一个名为
availableSize的Size参数,该参数为控件可用的最大尺寸,我们需要通过这个参数计算各个单元格的排列位置及尺寸,并根据排列情况返回一个控件的期望尺寸,以下为实现的大致流程。

1 通过Table的Height及Width参数计算出真实的尺寸;

2 读取Table的Tr属性,取出所有Td,并定义一个二维数组将所有Td存放进去(Td[n,n]),如果Td的RowSpan或ColSpan参数大于1则将被合并的位置存入一个null。

3 根据第2步的填充结果再定义一个存放坐标的二维数据(Size[n,n]);

4 测量Td子元素的尺寸,计算每个单元格实际尺寸,根据Td子元素尺寸计算是否需要压缩尺寸,计算完成后将单元格的尺寸存入第3步的数组中;

5 根据第3步保存的尺寸数据计算单元格跨行或跨列后的尺寸;

6 将计算出的Table实际尺寸返回给MeasureCore方法,以供下一步排列使用;

3.4.2
ArrangeCore()

该方法处理子控件的位置排列,循环调用每一个单元格的Arrange()方法,传入测量位置及尺寸就可以了。

3.4.3
OnRender()

该方法读取BorderColor、BgColor等参数画线及填充颜色,表格的外观都是由它画出来的。

四、

行效果

4.1 默认效果

<qs:TableWidth="50%"Height="50%"Align="Center"Border="1 solid red"Valign="Middle">
    <qs:Tr>
        <qs:Th>11</qs:Th>
        <qs:Th>12</qs:Th>
        <qs:Th>13</qs:Th>
        <qs:Th>14</qs:Th>
        <qs:Th>15</qs:Th>
        <qs:Th>16</qs:Th>
    </qs:Tr>
    <qs:Tr>
        <qs:Td>21</qs:Td>
        <qs:Td>22</qs:Td>
        <qs:Td>23</qs:Td>
        <qs:Td>24</qs:Td>
        <qs:Td>25</qs:Td>
        <qs:Td>26</qs:Td>
    </qs:Tr>
    <qs:Tr>
        <qs:Td>31</qs:Td>
        <qs:Td>32</qs:Td>
        <qs:Td>33</qs:Td>
        <qs:Td>34</qs:Td>
        <qs:Td>35</qs:Td>
        <qs:Td>36</qs:Td>
    </qs:Tr>
    <qs:Tr>
        <qs:Td>41</qs:Td>
        <qs:Td>42</qs:Td>
        <qs:Td>43</qs:Td>
        <qs:Td>44</qs:Td>
        <qs:Td>45</qs:Td>
        <qs:Td>46</qs:Td>
    </qs:Tr>
</qs:Table>

4.2 合并相邻的线

<qs:TableWidth="50%"Height="50%"Align="Center"Border="1 solid red collapse"Valign="Middle">
    <qs:Tr>
        <qs:Th>11</qs:Th>
        <qs:Th>12</qs:Th>
        <qs:Th>13</qs:Th>
        <qs:Th>14</qs:Th>
        <qs:Th>15</qs:Th>
        <qs:Th>16</qs:Th>
    </qs:Tr>
    <qs:Tr>
        <qs:Td>21</qs:Td>
        <qs:Td>22</qs:Td>
        <qs:Td>23</qs:Td>
        <qs:Td>24</qs:Td>
        <qs:Td>25</qs:Td>
        <qs:Td>26</qs:Td>
    </qs:Tr>
    <qs:Tr>
        <qs:Td>31</qs:Td>
        <qs:Td>32</qs:Td>
        <qs:Td>33</qs:Td>
        <qs:Td>34</qs:Td>
        <qs:Td>35</qs:Td>
        <qs:Td>36</qs:Td>
    </qs:Tr>
    <qs:Tr>
        <qs:Td>41</qs:Td>
        <qs:Td>42</qs:Td>
        <qs:Td>43</qs:Td>
        <qs:Td>44</qs:Td>
        <qs:Td>45</qs:Td>
        <qs:Td>46</qs:Td>
    </qs:Tr>
</qs:Table>

4.3 合并单元格

<qs:TableWidth="50%"Height="50%"Align="Center"Border="1 solid red collapse"Valign="Middle">
    <qs:Tr>
        <qs:Th>11</qs:Th>
        <qs:Th>12</qs:Th>
        <qs:Th>13</qs:Th>
        <qs:Th>14</qs:Th>
        <qs:Th>15</qs:Th>
        <qs:Th>16</qs:Th>
    </qs:Tr>
    <qs:Tr>
        <qs:TdColSpan="6">21</qs:Td>
    </qs:Tr>
    <qs:Tr>
        <qs:TdRowSpan="2">31</qs:Td>
        <qs:Td>32</qs:Td>
        <qs:TdColSpan="2"RowSpan="2">33</qs:Td>
        <qs:Td>35</qs:Td>
        <qs:TdRowSpan="2">36</qs:Td>
    </qs:Tr>
    <qs:Tr>
        <qs:Td>42</qs:Td>
        <qs:Td>45</qs:Td>
    </qs:Tr>
</qs:Table>

4.4 综合案例

<qs:TableWidth="50%"Height="50%"Align="Center"Border="1 solid Black collapse"Valign="Middle">
    <qs:TrHeight="40"Align="Center"BgColor="#FFAAAAAA"Valign="Middle">
        <qs:ThWidth="40"BgColor="#FFAAAAAA">11</qs:Th>
        <qs:ThWidth="10%">12</qs:Th>
        <qs:Th>13</qs:Th>
        <qs:Th>14</qs:Th>
        <qs:Th>15</qs:Th>
        <qs:Th>16</qs:Th>
    </qs:Tr>
    <qs:TrHeight="30%"Align="Center"BgColor="#FF5F5FF1"Valign="Middle">
        <qs:TdBgColor="#FFAAAAAA">21</qs:Td>
        <qs:Td>22</qs:Td>
        <qs:TdWidth="20%">23</qs:Td>
        <qs:Td>24</qs:Td>
        <qs:Td>25</qs:Td>
        <qs:Td>26</qs:Td>
    </qs:Tr>
    <qs:TrHeight="30%"Align="Center"BgColor="#FFEA8633"Valign="Middle">
        <qs:TdBgColor="#FFAAAAAA">31</qs:Td>
        <qs:Td>32</qs:Td>
        <qs:Td>33</qs:Td>
        <qs:Td>34</qs:Td>
        <qs:Td>35</qs:Td>
        <qs:Td>36</qs:Td>
    </qs:Tr>
    <qs:TrHeight="30%"Align="Center"BgColor="#FF5F5FF1"Valign="Middle">
        <qs:TdBgColor="#FFAAAAAA">41</qs:Td>
        <qs:Td>42</qs:Td>
        <qs:Td>43</qs:Td>
        <qs:Td>44</qs:Td>
        <qs:TdWidth="150">45</qs:Td>
        <qs:Td>46</qs:Td>
    </qs:Tr>
</qs:Table>

4.5 课表

<qs:TableWidth="600"Height="250"Align="Center"Border="1 solid black collapse"Valign="Middle">
    <qs:TrHeight="60"Align="Center"BgColor="#FFE5E5E5"Valign="Middle">
        <qs:ThColSpan="2">
            <TextBlockText="课时/日期" />
        </qs:Th>
        <qs:Th>星期一</qs:Th>
        <qs:Th>星期二</qs:Th>
        <qs:Th>星期三</qs:Th>
        <qs:Th>星期四</qs:Th>
        <qs:Th>星期五</qs:Th>
    </qs:Tr>
    <qs:TrAlign="Center"Valign="Middle">
        <qs:TdRowSpan="4">上午</qs:Td>
        <qs:TdWidth="100">第1节</qs:Td>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
    </qs:Tr>
    <qs:TrAlign="Center"Valign="Middle">
        <qs:Td>第2节</qs:Td>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
    </qs:Tr>
    <qs:TrAlign="Center"Valign="Middle">
        <qs:Td>第3节</qs:Td>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
    </qs:Tr>
    <qs:TrAlign="Center"Valign="Middle">
        <qs:Td>第4节</qs:Td>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
    </qs:Tr>
    <qs:TrAlign="Center"Valign="Middle">
        <qs:TdRowSpan="2">上午</qs:Td>
        <qs:TdWidth="100">第5节</qs:Td>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
    </qs:Tr>
    <qs:TrAlign="Center"Valign="Middle">
        <qs:Td>第6节</qs:Td>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
        <qs:Td/>
    </qs:Tr>
</qs:Table>

说明:文中用“像素”代替尺寸单位是为了便于理解,实际上WPF使用的是设备无关的
尺寸
单位,请注意分辨。

--------完结---------

作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!


file_line

https://github.com/ahfuzhang/file_line

Like
__FILE__/__LINE__
of C: use go generate to get source code line number at compile time.
像 C 语言里面的
__FILE__/__LINE__
宏一样:在编译期,通过
go generate
来得到源码行号。

我通常使用下面这个函数来获取源码的行号:

func SourceCodeLoc(callDepth int) string {
	_, file, line, ok := runtime.Caller(callDepth)
	if !ok {
		return ""
	}
	file = strings.ReplaceAll(file, "\\", "/")
	arr := strings.Split(file, "/")
	if len(arr) > 3 {
		file = strings.Join(arr[len(arr)-3:], "/")
	}
	return fmt.Sprintf("%s:%d", file, line)
}

func example(){
     Mylogger.Infof("[%s]something happens here", SourceCodeLoc(1))
}

这里的
runtime.Caller()
实在程序运行期间去计算程序对应的源码行的,必然会带来性能损耗。
这种需求完全可以在编译期间实现,最终我发现使用 go ast 库能够简单的达成这一功能。

How to use

  1. 安装:
go install github.com/ahfuzhang/file_line@latest
  1. 编写代码,在需要使用行号的地方使用这样的place holder:
func myCode(){
    Mylogger.Infof(“%s: something happens here”, “[file.go:123]")
}
  1. 在程序的入口出加上 go generate指令:
//go:generate file_line -src=./

func main() {
	fmt.Println("use a place holder:", "[file.go:123]")
}
  1. 在编译前执行
    go generate
  • 所有的处于函数调用参数位置的 place holder 会被替换为正确的文件名和行号
  • 也可以直接在命令行执行 file_line -src=./
  1. 执行
    go build


Have Fun.