您现在的位置是:网站首页> C#技术
C#开源库及辅助程序
- C#技术
- 2024-09-07
- 1018人已阅读
C#开源库及辅助程序
DotNetDetour - 万能的开源 .NET 代码 Hook 神器
WeiXinMPSDK微信全平台 SDK Senparc.Weixin for C#,支持 .NET Framework 及 .NET Core、.NET 6.0、.NET 8.0。
BBDown Bilibili Downloader. 一款命令行式哔哩哔哩下载器
SunnyUI.Net, 基于.Net 4.0+、.Net 6 框架的 C# WinForm 开源控件库、工具类库、扩展类库、多页面开发框架
PaddleSharp支持14种OCR语言模型的按需下载,允许旋转文本角度检测,180度文本检测,同时也支持表格识别
PaddleOCRSarp是一个基于百度飞桨PaddleOCR的C++代码封装
CoreShop基于 Asp.Net Core 8.0、Uni-App开发,支持可视化布局的小程序商城系统
redis-windows实时编译适用于Windows系统的Redis最新版本
llcom功能强大的串口工具。支持Lua自动化处理、串口调试、串口监听、串口曲线、TCP测试、MQTT测试、编码转换、乱码恢复等功能
TouchSocket是.Net超轻量级的网络通信框架。包含了 tcp、udp、ssl、http、websocket、rpc、jsonrpc、webapi、xmlrpc等一系列的通信模块
HslControls控件库的使用demo,HslControls是一个工业物联网的控件库
StabilityMatrix为Stable Diffusion提供易于使用的软件包管理
PdfiumViewer或PdfSharp来实现流式加载PDF并显示
FaceRecognitionDotNet。这是一个基于 dlib 的人脸识别库的.NET 封装
N_m3u8DL-RE跨平台的DASH/HLS/MSS下载工具。支持点播、直播(DASH/HLS)。
ConfuserEx是一个功能强大的开源.NET程序集保护工具
MaterialSkin for .NET WinForms
C#调用ffmpeg库AForge.Video.FFMPEG
FFplay成功地移动到我的Winform中,如何将其设置为无边框
QuestPDF处理PDF库
using System;
using System.Windows.Forms;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace NETtoPDF
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A5);
page.Margin(2, Unit.Centimetre);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(12).FontFamily("黑体"));
page.Background().Image("D:\VS2022\NETtoPDF\167979257449892.jpg");
page.Header();
page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(x =>
{
x.Spacing(20);
x.Item().Text("这是一个PDF文件").FontSize(26).FontColor(Colors.Blue.Medium);
x.Item().Text("这是使用QuestPDF NET组件生成的PDF文件,文件的大小可能会受到字体的样式影响。");
x.Item().Image("D:\VS2022\NETtoPDF\167979257449892.jpg");
});
page.Footer()
.AlignCenter()
.Text(x =>
{
x.Span("当前页 ");
x.CurrentPageNumber();
});
});
}).GeneratePdf("hello.pdf");
}
}
}
仔细查看参考文献3中包含中文字体的示例代码,发现其设置黑体时使用的FontFamily名称是simhei,百度后才知道该名称是黑体字体对应的ttf文件名。于是到windows\Fonts文件夹下找到宋体的字体文件名,如下所示:
??重新设置代码中的FontFamily名,在运行程序,即可生成显示正常的pdf文档,如下图所示:
page.Header()
.Text("QuestPDF测试")
.SemiBold().FontFamily("simsun").FontSize(36).FontColor(Colors.Blue.Medium);
视觉元素
Text
一般使用默认或自定义样式绘制文本,文本总是占用尽可能少的空间。如果文本字符串很长,则元素可能占据整个宽度并中断到下一行。此元素支持分页。
1、“文本”方法,如下所示:
C#
//直接输出文字,需要文件配置默认的字体(中文)
.Text("这是一个PDF文件");
//设置字体颜色、大小
.Text("这是一个PDF文件").FontColor("#F00").FontSize(24)
2、“文本块”方法,如下所示:
C#
//文本块方法
.Text(text =>
{
text.Span("关关雎鸠,在河之洲。");
//下划线
text.Span("窈窕淑女,君子好逑。").Underline();
});
3、基本字体样式
C#
//字体颜色
x.Item().Text("测试字体基本样式").FontColor("#F00");
//字体格式
x.Item().Text("测试字体基本样式").FontFamily("黑体");
//字体字号
x.Item().Text("测试字体基本样式").FontSize(24);
//行间距
x.Item().Text("测试字体基本样式").LineHeight(1.5f);
//斜体
x.Item().Text("测试字体基本样式").Italic();
//字体背景色
x.Item().Text("测试字体基本样式").BackgroundColor("#f00");
//中划线
x.Item().Text("测试字体基本样式").Strikethrough();
//下划线
x.Item().Text("测试字体基本样式").Underline();
//下标
x.Item().Text(t =>
{
t.Span("CM");
t.Span("2").Subscript();
});
//上标
x.Item().Text(t =>
{
t.Span("CM");
t.Span("2").Superscript();
});
//字体排列方式
x.Item().Text(text =>
{
//左对齐
text.AlignLeft();
//居中
text.AlignCenter();
//右对齐
text.AlignRight();
text.Span("Sample text");
});
4、定义默认字体样式
C#
//用于文件页面默认样式设定
container.Page(page =>
{
page.DefaultTextStyle(x => x.FontSize(12).FontFamily("黑体"));
});
//用于文本块默认样式设定
x.Item().Text(t =>
{
t.DefaultTextStyle(tt => tt.FontSize(12).FontFamily("黑体").FontColor(Colors.Blue.Medium));
t.Line("第一行文本");
t.Line("第二行文本").Underline();
});
5、字体由细到粗
注意,并非所有字体都支持每种样式,QuestPDF将匹配最接近的可用样式
C#
.Weight(FontWeight.Normal)
.Thin()
.ExtraLight()
.Light()
.NormalWeight()
.Medium()
.SemiBold()
.Bold()
.ExtraBold()
.Black()
.ExtraBlack()
6、一些特殊的用法
C#
//设置样式
var highlight = TextStyle.Default.BackgroundColor(Colors.Green.Lighten3);
//赋值样式Text
Text.Span("E=mc").Style(highlight);
//换行
Text.EmptyLine();
//行与字母设置方式
.Column(column =>
{
//设置间距数组
var lineHeights = new[] { 0.8f, 1f, -1.5f };
//遍历间距数组
foreach (var lineHeight in lineHeights)
{
//行项目
column
//行项目
.Item()
//边框1
.Border(1)
//边距
.Padding(10)
//输出内容
.Text("行内容")
//字体大小
.FontSize(16)
//行间距,可以为负值
.LineHeight(lineHeight)
//字符间距,可以为负值
.LetterSpacing(lineHeight);
}
});
7、定义自定义样式类
C#
public static class Typography
{
public static TextStyle Title => TextStyle
.Default
.FontType("Helvetica")
.FontColor(Colors.Black)
.FontSize(20)
.Bold();
public static TextStyle Headline => TextStyle
.Default
.FontType("Helvetica")
.FontColor(Colors.Blue.Medium)
.FontSize(14);
public static TextStyle Normal => TextStyle
.Default
.FontType("Helvetica")
.FontColor("#000000")
.FontSize(10)
.LineHeight(1.25f)
.AlignLeft();
}
然后,可以通过以下方式使用预定义的排版:
C#
.Text("自定义的文本样式").Style(Typography.Headline)
Images
此元素可用于在文档中放置图像。
默认情况下,“图像”会保留图像的纵横比。
图像被加载到SkiaSharp.image对象中。请注意,所有限制都是派生的。例如,可用的图像格式可能因平台而异。
您可以使用任何常见光栅格式的图像,例如JPG、PNG、BMB等。
C#
// 可以提供如下图像:
// 1) 二进制数组
byte[] imageData = File.ReadAllBytes("167979257449892.jpg");
x.Item().Image(imageData);
// 2) 文件名称
x.Item().Image("167979257449892.jpg");
// 3) 文件流
var stream = new FileStream("167979257449892.jpg", FileMode.Open);
x.Item().Image(stream);
C#使用Tesseract进行Ocr识别
1.Nuget搜索Tesseract
2.项目安装Tesseract
3.引用命名空间
using Tesseract;
4.上Github下载别人的训练库
GitHub - tesseract-ocr/tessdata: Trained models with support for legacy and LSTM OCR engine
https://github.com/tesseract-ocr/tessdata
我这里下载中文的chi_sim.traineddata,放到了D盘根目录
5.选择图片进行识别
我把图片命名为image.png放在了D盘根目录
//图片文件路径
string imageFileName = @"D:\image.png";
//创建位图对象
Bitmap image = new Bitmap(imageFileName);
//Tesseract.Page
Page page = new TesseractEngine(@"D:\", "chi_sim", EngineMode.Default).Process(PixConverter.ToPix(image));
//释放程序对图片的占用
image.Dispose();
//打印识别率
Console.WriteLine(String.Format("{0:P}", page.GetMeanConfidence()));
//打印识别文本 //替换'/n'为'(空)'//替换'(空格)'为'(空)'
Console.WriteLine(page.GetText().Replace("\n", "").Replace(" ", ""));
识别率为百分之84,识别文字为立白liby
DotNetDetour - 万能的开源 .NET 代码 Hook 神器
DotNetDetour是一个用于.net方法hook的类库
特点
• 支持32bit和64bit的.net程序
• 支持静态方法,实例方法、属性方法、泛型类型的方法、泛型方法的hook
• 支持.net基础类库方法的hook
• 无任何性能影响,无需知道和改动被hook的方法源码
基础示例
1.git clone本项目最新源码使用;或者NuGet安装(可能未及时更新):Install-Package DotNetDetour, 或者:Install-Package kissstudio.DotNetDetour。
2.参考以下例子实现IMethodHook接口,使用特性标记要Hook的方法
namespace Test.Solid {
//假设有一个已存在的类(并且无法修改源码,如.Net框架的方法)
public class SolidClass{
public string Run(string msg){
return msg+"(run)";
}
}
}
namespace Test{
//我们自行实现一个类来修改Run方法的行为,此类用IMethodHook接口修饰
public class MyClass:IMethodHook{
//我们实现一个新Run方法,并标记为HookMethod,覆盖SolidClass中的Run方法
[HookMethod("Test.Solid.SolidClass")]
public string Run(string msg){
return "Hook " + Run_Original(msg);
}
//实现一个占位方法,此方法代表被Hook覆盖的原始方法
[OriginalMethod]
public string Run_Original(string msg){
return null; //这里写什么无所谓,能编译过即可
}
}
}
3.在程序中执行安装操作(只需运行一次即可),最佳运行时机:必须在被Hook方法被调用前执行,最好程序启动时运行一次即可。
MethodHook.Install();
4.当执行到被Hook的方法时,该调用将被转到我们的Hook方法执行:
var msg=new SolidClass().Run("Hello World!");
//Hook Hello World!(run)
Hook场景
普通方法Hook
静态和非静态的普通方法Hook操作都是一模一样的,两步到位:新建一个类实现IMethodHook接口,编写普通Hook方法,用HookMethod特性标记此方法,有无static修饰、返回值类型(仅针对引用性质的类型,非int等值类型)不同都不影响,但参数签名要和被Hook的原始方法一致,值类型和引用类型尽量不要混用。
第一步:新建一个类实现IMethodHook接口
我们编写的Hook方法所在的类需要实现IMethodHook接口,此接口是一个空接口,用于快速的查找Hook方法。
或者使用IMethodHookWithSet接口(算Plus版吧),此接口带一个HookMethod(MethodBase method)方法,这个类每成功进行一个Hook的初始化,就会传入被Hook的原始方法(可判断方法名称来确定是初始化的哪个方法),这个方法可用于获取方法所在的类(如:私有类型),可用于简化后续的反射操作;注意:此方法应当当做静态方法来进行编码。
第二步:编写Hook方法,用HookMethod特性标记
HookMethod(type,targetMethodName,originalMethodName) ,type参数支持:Type类型对象、类型完全限定名。如果能直接获取到类型对象,就使用Type类型对象;否则必须使用此类型的完全限定名(如:私有类型),如:System.Int32、System.Collections.Generic.List`1[[System.String]]。
[HookMethod("Namespace.xxx.MyClass", "TargetMethodName", "OriginalMethodName")]
public string MyMethod(string param){...}
[HookMethod(typeof(MyClass))]
public string MyMethod(string param){...}
如果我们的方法名称和被Hook的目标方法名称一致,无需提供targetMethodName参数。
如果我们提供目标原始方法的占位方法OriginalMethod,并且名称为目标原始方法名称 + _Original,或者当前类内只有一个Hook方法,无需提供originalMethodName参数。
注意:方法参数
参数签名要和被Hook的原始方法一致,如果不一致将导致无法找到原始方法(原因:存在重载方法无法确认是哪个的问题)。
如果存在我们无法使用的参数类型的时候(如:私有类型),我们可以用object等其他引用类型代替此类型(注意不要用值类型,否则可能出现内存访问错误),并把此参数用RememberType进行标记:
//目标方法:
public string SolidMethod(MyClass data, int code){...}
//我们的Hook方法:
public string MyMethod([RememberType("Namespace.xxx.MyClass")]object data, int code){...}
可选:提供OriginalMethod特性标记的原始方法
如果我们还想调用被Hook的原始方法,我们可以提供一个占位方法,此方法用OriginalMethod进行标记即可。此方法只起到代表原始方法的作用,不需要可以不提供,要求:参数签名必须和我们写的Hook方法一致(原因:存在重载方法无法确认是哪个的问题)。
此方法默认名称格式为目标原始方法名称 + _Original,不使用这个名称也可以,但如果使用其他名称并且当前类中有多个Hook方法,必须在Hook方法HookMethod特性中进行设置originalMethodName进行关联。
[OriginalMethod]
public string SolidMethod_Original(object data, int code){
可选:给我们的Hook方法传递参数
我们编写Hook方法是在被Hook的原始方法被调用时才会执行的,我们可能无法修改调用过程的参数(如果是能修改方法的话就跳过此节),虽然我们编写的Hook方法可以是非静态方法,但我们应当把它当静态方法来看待,虽然可以用属性字段(非静态的也当做静态)之类的给我们的Hook方法传递数据,但如果遇到并发,是不可靠的。
我们可以通过当前线程相关的上下文来传递数据,比如:HttpContext、CallContext、AsyncLocal、ThreadLoacl。推荐使用CallContext.LogicalSetData来传递数据,如果可以用HttpContext就更好了(底层也是用CallContext.HostContext来实现的)。ThreadLoacl只能当前线程用,遇到异步、多线程就不行了。AsyncLocal当然是最好的,但稍微低些版本的.Net Framework还没有这个。
[HookMethod("Namespace.xxx.MyClass", "TargetMethodName", "OriginalMethodName")]
public string MyMethod(string param){
if (CallContext.LogicalGetData("key") == (object)"value") {
//执行特定Hook代码
return;
}
//执行其他Hook代码
...
}
//调用
CallContext.LogicalSetData("key", "value");
new MyClass().MyMethod("");
CallContext.LogicalSetData("key", null);
注:虽然大部分多线程、异步环境下调用上下文是会被正确复制传递的,但如果哪里使用了ConfigeAwait(false)或者其他影响上下文的操作(定时回调、部分异步IO回调好像也没有传递),当我们的Hook方法执行时,可能上下文数据并没有传递进来。
异步方法Hook
异步方法的Hook方法需要用async来修饰、返回Task类型,其他和普通方法Hook没有区别。
小提醒:不要在存在SynchronizationContext(如:HttpContext、UI线程)的线程环境中直接在同步方法中调用异步方法,真发生异步行为时100%死锁,可以强制关闭SynchronizationContext来规避此种问题,但会引发一系列问题。如果使用过程中发生死锁,跟我们进行的Hook操作没有关系。
[HookMethod(typeof(MyClass))]
public async Task<int> MyMethodAsync() {...}
//异步环境调用
val=await new MyClass().MyMethodAsync();
//同步环境调用
var bak = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
try {
val=new MyClass().MyMethodAsync().Result;
} finally {
SynchronizationContext.SetSynchronizationContext(bak);
}
属性Hook
属性其实是get_xxx()名称的普通方法,比如MyProperty属性Hook get_MyProperty()这个普通方法即可。
[HookMethod("Namespace.xxx.MyClass")]
public string get_MyProperty(){...}
[OriginalMethod]
public string get_MyProperty_Original(){...}
或者在get块上方进行标记,规则和普通方法一致:
public string MyProperty{
[HookMethod("Namespace.xxx.MyClass")]
get{ ... }
}
public string MyProperty_Original{
[OriginalMethod]
get{ ... }
}
注:Hook属性时有可能能成功设置此Hook,但不一定会执行我们的代码,可能是编译过程中优化了整个调用过程,跳过了部分属性方法,直接返回了最深层次的调用值,如下面这种类似的属性获取方式:
int A{get{return B;}}
int B{get{return C;}}
int C{get{return 123;}}
我们Hook A属性,能成功设置Hook方法,但我们调用A属性时,并不会执行我们的Hook方法。换B也不行,只有Hook C才行。也许是编译的时候把A、B的调用直接优化成了对C的调用,我们只需要对最深层次的属性调用进行Hook就能避免此问题。(这个只是演示可能会出现的问题,我们自己特意写代码去测试并不能复现)。
构造方法Hook
我们编写个返回值为void、方法名称为类名称的普通方法即可实现。如果方法名称无法使用类名称时,需在HookMethod中设置targetMethodName为.ctor。其他规则和普通方法一致。
[HookMethod("Namespace.xxx.MyClass")]
public void MyClass(string param) {
...
MyClass_Original(param);//可选调用自身实例化方法
...
}
[OriginalMethod]
public void MyClass_Original(string param) {}
泛型类的方法Hook
形如class MyClass<T>{ T MyMethod(T param, object param2){...} }这种泛型,对里面的方法进行Hook。泛型类中方法的Hook和普通方法Hook没有多大区别,只是在提供HookMethod特性的type参数时需要对类型具体化,比如调用的地方使用的是int类型,那么我们就Hook int类型的此类:typeof(MyClass<int>)、Namespace.xxx.MyClass`1[[System.Int32]],其他和普通方法规则相同。
由于存在引用类型和值类型两种类型,并且表现不一致,我们在具体化时要分开对待。
值类型泛型参数
每种使用到的值类型泛型参数的具体类型都需要单独实现Hook,int、bool等为值类型都要单独实现,如int类型写法:
[HookMethod("Namespace.xxx.MyClass`1[[System.Int32]]")]
public int MyMethod(int param, object param2) {
引用类型泛型参数
每种使用到引用类型参数的具体类型都共用一个Hook,注意是:同一个泛型类中的同一个方法只能用一个相同方法进行Hook,string、普通object等都是引用类型都共用一个Hook,如string类型写法:
[HookMethod("Namespace.xxx.MyClass`1[[System.Object]]")]
public object MyMethod(object param, object param2) {
if(param is string){
... //string 类型实现代码
} else if(param is xxxx){
... //其他引用类型实现代码
}
实现原理
1.为何想做这个
说到hook大家都应该不陌生,就是改变函数的执行流程,让本应该执行的函数跑到另一个函数中执行,这是个很有用也很有趣的功能(例如获取函数参数信息,改变函数执行流程,计算函数执行时间等等),杀软中主防的原理就是hook,通过hook拦截函数获取参数信息来判断是否是危险行为,但这类程序大多是C++的,一直以来我都想实现可以hook .net函数的库,网上搜索了很多,但都不理想,所以想自己实现一个。
2.实现原理
我采用的是inline hook的方式,因为我对.net虚拟机以及一些内部的结构并不是很熟悉,并且有些东西的确找不到任何文档,所以就采用原生代码的inline hook的方式来实现。
首先说一下inline hook的基本原理,它是通过修改函数的前5字节指令为jmp xxxxxxxx来实现的,例如一个C#方法:
用windbg调试查看方法信息:
查看已经jit了的原生代码:
这里的地址(0x008c0640)可以通过MethodInfo.MethodHandle.GetFunctionPointer().ToPointer()方法获取。
到了这里,我们就知道了修改从push ebp开始的5个字节为jmp跳转指令,跳入我们自己的函数就可以达到hook的目的,但执行到我们的函数后,如果我们并不是要拦截执行流程,那么我们最终是需要再调用原函数的,但原函数已经被修改了,这会想到的办法就是恢复那修改的5字节指令,但这又会引发另一个问题,就是当我们恢复时,正好另一个线程调用到这个函数,那么程序将会崩溃,或者说漏掉一次函数调用,修改时暂停其他线程并等待正跑在其中的CPU执行完这5字节再去恢复指令也许是个不错的办法,但感觉并不容易实现,而且影响性能,所以我放弃了这种办法。
那么如何才能调用修改前的函数呢,我首先想到是C中写裸函数的方式,即自己用汇编拼出来一个原函数再执行:
原函数前5字节指令+jmp跳转指令
但其实这也是不可行的,聪明的人已经发现,图中所示的函数的前5字节并不是一个完整的汇编指令,不同的函数,长度都不一样,.net的函数并不像某些原生函数那样,会预留mov edi,edi这样的正好5字节的指令,我先想到的是复制函数的所有汇编指令生成新的函数,但这样也会出问题,因为像E8,E9这样的相对跳转指令,如果指令地址变了,那么跳转的位置也就变了,程序就会崩溃,所以这也不可行。
到了这里,我有些不耐烦了,毕竟我是要hook所有函数的,而不是某个固定的函数,而函数入口的指令又不相同,这可怎么办,难道我需要计算出大于等于5字节的最小完整汇编指令长度?
按照这个思路,最终找到了一个用C写的反汇编库(BlackBone),其中提供了类似的方法,我稍作了修改后试用了下,的确不错,可以准确求出汇编指令长度,例如
push ebp
mov ebp,esp
mov eax,dword ptr ds:[33F22ACh]
求出值是9,这样我根据求出的值动态拼接一个函数出来即可,哈哈,到了这里,感觉实现的差不多了,但没想到64位下又给了我当头一棒,之前的原函数指令可以写成:
大于等于5字节的最小完整汇编指令+jmp跳转指令 即可构成我们的原函数
但我们知道,C#中要想执行汇编,是需要用Marshal.AllocHGlobal来分配非托管空间的,而这样分配的地址与我们要跳转到的原函数的地址在64位下是超过2GB地址范围的,一般的跳转指令是无法实现的,所以想到了用ret指令实现,而64位地址又不能直接push,所以最后写出如下汇编:
push rax
mov rax,target_addr
push rax
mov rax,qword ptr ss:[rsp+8]
ret 8
由于某些C#函数竟然第一行就是修改rax寄存器的值,所以只能是先保存rax,推入堆栈后再恢复,这里汇编操作就方便多了,之前实现另一个东西,用到IL指令,但发现只有dup这种复制栈顶元素的指令,却没有获取堆栈中某个非栈顶元素值的指令,所以说还是汇编灵活啊,想怎么写就怎么写,啥都能实现。
最后就是这个原函数的调用过程了,因为是动态拼接的函数,所以想到的就是用Marshal.GetDelegateForFunctionPointer转成委托来执行,后来发现不对,因为我虽然拼接的是汇编,而这个汇编是C#方法jit后的汇编,这个并不是C方法编译后的汇编,通过把非托管指针转换为委托的方式运行函数是会添加很多不需要的操作的,例如托管类型与非托管类型的转换,但我拼接出的函数是不需要这些过程的,这个怎么办,看来只能用调用C#普通函数的方式调用,这个怎么实现呢,其实很好办,只需写一个空壳函数,然后修改这个函数的方法表中的原生指令指针即可,具体方法如下:
*((ulong*)((uint*)method.MethodHandle.Value.ToPointer() + 2)) = (ulong)ptr;
method是空壳函数的MethodInfo, ptr是动态拼接的原函数的地址
好,到了这里就基本完成核心功能了,最不好处理的就是这个原函数调用,我的完整的64位原函数指令拼接就实现了,代码很少,如下所示:
byte[] jmp_inst =
{
0x50, //push rax
0x48,0xB8,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90, //mov rax,target_addr
0x50, //push rax
0x48,0x8B,0x44,0x24,0x08, //mov rax,qword ptr ss:[rsp+8]
0xC2,0x08,0x00 //ret 8
};
protected override void CreateOriginalMethod(MethodInfo method)
{
uint oldProtect;
var needSize = NativeAPI.SizeofMin5Byte(srcPtr);
byte[] src_instr = new byte[needSize];
for (int i = 0; i < needSize; i++)
{
src_instr[i] = srcPtr[i];
}
fixed (byte* p = &jmp_inst[3])
{
*((ulong*)p) = (ulong)(srcPtr + needSize);
}
var totalLength = src_instr.Length + jmp_inst.Length;
IntPtr ptr = Marshal.AllocHGlobal(totalLength);
Marshal.Copy(src_instr, 0, ptr, src_instr.Length);
Marshal.Copy(jmp_inst, 0, ptr + src_instr.Length, jmp_inst.Length);
NativeAPI.VirtualProtect(ptr, (uint)totalLength, Protection.PAGE_EXECUTE_READWRITE, out oldProtect);
RuntimeHelpers.PrepareMethod(method.MethodHandle);
*((ulong*)((uint*)method.MethodHandle.Value.ToPointer() + 2)) = (ulong)ptr;
}
3.类库开发所用到的语言 之前我说,我的这个库是完全用C#实现的,但其中的确用到了一个C写的反汇编库,于是我用C#把那个库重写了一遍,说来也简单,C的代码粘过来,C#启用unsafe代码,改了10分钟就好了,真心是非常方便,毕竟C#是支持指针和结构体的,而且基础类型非常丰富,这里得给C#点个赞!
C#的dapper使用
Dapper的安装[
方法一:使用NuGet安装
打开visual studio的项目,依次点击工具,NuGet包管理器,管理解决方案的NuGet程序包;
再点击浏览,搜索dapper,点击搜索结果中的Dapper,勾选项目,选择安装;
在解决方案管理器中点击项目,查看引用,如果有Dapper,说明安装成功。
方法二:直接在官网[2]下载源代码,加入项目。这种方法哈希君没有试,不过可以参考链接Dapper快速学习
插入操作
插入代码文本如下。@Name的意思是自动将person里的Name值绑定上去。
public static int Insert(Person person)
{
using (IDbConnection connection = new SqlConnection(connectionString))
{
return connection.Execute("insert into Person(Name,Remark) values(@Name,@Remark)", person);
}
}
批量插入:
/// <summary>
/// 批量插入Person数据,返回影响行数
/// </summary>
/// <param name="persons"></param>
/// <returns>影响行数</returns>
public static int Insert(List<Person> persons)
{
using (IDbConnection connection = new SqlConnection(connectionString))
{
return connection.Execute("insert into Person(Name,Remark) values(@Name,@Remark)", persons);
}
}
删除操作
public static int Delete(Person person)
{
using (IDbConnection connection = new SqlConnection(connectionString))
{
return connection.Execute("delete from Person where id=@ID", person);
}
}
public static int Delete(List<Person> persons)
{
using (IDbConnection connection = new SqlConnection(connectionString))
{
return connection.Execute("delete from Person where id=@ID", persons);
}
}
修改操作
public static int Update(Person person)
{
using (IDbConnection connection = new SqlConnection(connectionString))
{
return connection.Execute("update Person set name=@name where id=@ID", person);
}
}
public static int Update(List<Person> persons)
{
using (IDbConnection connection = new SqlConnection(connectionString))
{
return connection.Execute("update Person set name=@name where id=@ID", persons);
}
}
查询操作
/// <summary>
/// 无参查询所有数据
/// </summary>
/// <returns></returns>
public static List<Person> Query()
{
using (IDbConnection connection = new SqlConnection(connectionString))
{
return connection.Query<Person>("select * from Person").ToList();
}
}
/// <summary>
/// 查询指定数据
/// </summary>
/// <param name="person"></param>
/// <returns></returns>
public static Person Query(Person person)
{
using (IDbConnection connection = new SqlConnection(connectionString))
{
return connection.Query<Person>("select * from Person where id=@ID", person).SingleOrDefault();
}
}
Dapper的复杂操作
查询的In操作
/// <summary>
/// In操作
/// </summary>
public static List<Person> QueryIn()
{
using (IDbConnection connection = new SqlConnection(connectionString))
{
var sql = "select * from Person where id in @ids";
//参数类型是Array的时候,dappper会自动将其转化
return connection.Query<Person>(sql, new { ids = new int[2] { 1, 2 }, }).ToList();
}
}
public static List<Person> QueryIn(int[] ids)
{
using (IDbConnection connection = new SqlConnection(connectionString))
{
var sql = "select * from Person where id in @ids";
//参数类型是Array的时候,dappper会自动将其转化
return connection.Query<Person>(sql, new { ids }).ToList();
}
}
多语句操作
为此我们引入以下Book类,同样在数据库里设置这个表。
public class Book
{
public int ID { get; set; }
public int PersonID { get; set; }
public string BookName { get; set; }
}
/// <summary>
/// 多语句操作
/// </summary>
public static void QueryMultiple()
{
using (IDbConnection connection = new SqlConnection(connectionString))
{
var sql = "select * from Person; select * from Book";
var multiReader = connection.QueryMultiple(sql);
var personList = multiReader.Read<Person>();
var bookList = multiReader.Read<Book>();
multiReader.Dispose();
}
}
Join操作
我们是面向对象编程,所以一个对象里面会有许多其他子对象,这个子对象里面又有其自己的子对象,这种关系在数据库里的表示就是外键。
比如我们有一本书book,它有主人person,book是一个对象,主人又是一个对象。
public class BookWithPerson
{
public int ID { get; set; }
public Person Pers { get; set; }
public string BookName { get; set; }
}
我们自然想要一个方法把数据库里复杂的外键关系转成我们需要的对象BookWithPerson,所有我们需要的信息都存在里面,取数据的时候只要找这个对象取数据就行了,比如我们需要一本书的主人的姓名,我们只需要bookWithPerson.Pers.Name。如果是一对多的关系我们用数组,如果是多对多我们加一层mapping。
现在我们想根据书的ID查询书的信息,包括主人信息。那么
public static BookWithPerson QueryJoin(Book book)
{
using (IDbConnection connection = new SqlConnection(connectionString))
{
var sql = @"select b.id,b.bookName,p.id,p.name,p.remark
from Person as p
join Book as b
on p.id = b.personId
where b.id = @id;";
var result = connection.Query<BookWithPerson, Person, BookWithPerson>(sql,
(bookWithPerson, person) =>
{
bookWithPerson.Pers = person;
return bookWithPerson;
},
book);
//splitOn: "bookName");
return (BookWithPerson)result;
}
}
其中,Query的三个泛型参数分别是委托回调类型1,委托回调类型2,返回类型。形参的三个参数分别是sql语句,map委托,对象参数。所以整句的意思是先根据sql语句查询;同时把查询的person信息赋值给bookWithPerson.Pers,并且返回bookWithPerson;book是对象参数,提供参数绑定的值。
最终整个方法返回BookWithPerson,这样我们所需要的所有信息就有了。
C# 超简单的离线人脸识别库
一个基于 SeetaFace6 的 .NET 人脸识别解决方案
本项目受到了 SeetaFaceEngine.Net 的启发
开源、免费、跨平台 (win/linux)
获取人脸信息
using SkiaSharp;
using System;
using ViewFaceCore.Core;
using ViewFaceCore.Model;
namespace ViewFaceCore.Demo.ConsoleApp
{
internal class Program
{
private readonly static string imagePath = @"images/Jay_3.jpg";
static void Main(string[] args)
{
using var bitmap = SKBitmap.Decode(imagePath);
using FaceDetector faceDetector = new FaceDetector();
FaceInfo[] infos = faceDetector.Detect(bitmap);
Console.WriteLine($"识别到的人脸数量:{infos.Length} 个人脸信息:\n");
Console.WriteLine($"No.\t人脸置信度\t位置信息");
for (int i = 0; i < infos.Length; i++)
{
Console.WriteLine($"{i}\t{infos[i].Score:f8}\t{infos[i].Location}");
}
Console.ReadKey();
}
}
}
PuppeteerSharp使用
使用netget安装,PuppeteerSharp采用netstandard2.0,支持 .NET Framework 4.6.1以上
修改C#语言版本改文件.csproj,加入
<PropertyGroup>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
例子
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using PuppeteerSharp;
namespace HelloNET
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
// _ = Hello();
//MessageBox.Show("马上返回不等待Hello执行完毕");
string str=await Hello();
MessageBox.Show("Hello已经执行完毕,返回:"+str);
}
//该异步函数返回string
async Task <string>Hello()
{
var options = new LaunchOptions { Headless = true };
Console.WriteLine("Downloading chromium");
using var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync();
Console.WriteLine("Navigating google");
await using var browser = await Puppeteer.LaunchAsync(options);
await using var page = await browser.NewPageAsync();
await page.GoToAsync("https://www.google.com");
Console.WriteLine("Generating PDF");
await page.PdfAsync(Path.Combine(Directory.GetCurrentDirectory(), "google.pdf"));
Console.WriteLine("Export completed");
return "我是执行完后返回的串";
}
}
}
在 PuppeteerSharp 中,你可以使用 Page 对象提供的方法来模拟用户操作。以下是一些常见的模拟用户操作的示例:
点击元素:
await page.ClickAsync("#myButton");
输入文本:
await page.TypeAsync("#myInput", "Hello, World!");
提交表单:
await page.SubmitAsync("#myForm");
模拟键盘输入:
await page.Keyboard.TypeAsync("Hello, World!");
模拟鼠标移动:
await page.Mouse.MoveAsync(x: 100, y: 200);
滚动页面:
await page.EvaluateExpressionAsync("window.scrollTo(0, 500)");
等待元素出现:
await page.WaitForSelectorAsync("#myElement");
注入JQuery
await page.EvaluateFunctionAsync(@"() => {
var script = document.createElement('script');
script.src = 'https://code.jquery.com/jquery-3.6.0.min.js';
document.head.appendChild(script);
}");
后续可使用
var result = await page.EvaluateFunctionAsync<string>(@"() => {
return $('h1').text();
}");
Console.WriteLine(result); // 输出 <h1> 元素的文本内容
在 PuppeteerSharp 中,等待页面加载完毕可以使用以下方法:
使用 Page.WaitForNavigationAsync() 方法:
这个方法可以用于等待页面导航完成,包括页面加载、重定向和单页应用程序的页面切换。
await page.WaitForNavigationAsync();
你还可以通过传递选项来指定等待的条件,例如等待特定的 URL 或网络状态:
await page.WaitForNavigationAsync(new NavigationOptions
{
WaitUntil = new[] { WaitUntilNavigation.Networkidle0 }
});
上述示例中,WaitUntilNavigation.Networkidle0 表示等待网络处于空闲状态,即没有网络连接活动。
使用 Page.WaitForSelectorAsync() 方法:
这个方法可以用于等待特定的选择器匹配的元素出现在页面上。
await page.WaitForSelectorAsync("#myElement");
可以通过传递选项来指定等待的条件,例如等待元素可见、可交互或不再隐藏:
await page.WaitForSelectorAsync("#myElement", new WaitForSelectorOptions
{
Visible = true
});
上述示例中,Visible = true 表示等待元素在页面上可见。
使用 Page.WaitForFunctionAsync() 方法:
这个方法可以用于等待通过自定义 JavaScript 函数返回 true 的条件。
await page.WaitForFunctionAsync("() => document.readyState === 'complete'");
上述示例中,等待直到页面的 document.readyState 变为 'complete',即表示页面加载完毕。
演示如何在 PuppeteerSharp 中指定 Chrome 浏览器的可执行文件路径:
var options = new LaunchOptions
{
ExecutablePath = "path/to/chrome.exe" // 指定 Chrome 可执行文件的路径
};
using (var browser = await Puppeteer.LaunchAsync(options))
{
// 在这里使用 PuppeteerSharp 控制 Chrome 浏览器
// ...
要在 PuppeteerSharp 中保存用户登录状态以便复用,你可以使用下面的方法:
使用 Page.Cookies 属性保存和加载 cookies:
在用户成功登录后,你可以使用 Page.Cookies.GetCookiesAsync() 方法获取当前页面的 cookies,并将其保存到一个变量中。例如:
var cookies = await page.GetCookiesAsync();
// 将 cookies 保存到文件或数据库中
然后,在后续的会话中,你可以使用 Page.Cookies.SetCookiesAsync() 方法将保存的 cookies 加载到新的页面中,以恢复用户的登录状态。例如:
// 从文件或数据库中加载保存的 cookies
// var savedCookies = LoadCookiesFromStorage();
await page.SetCookiesAsync(savedCookies);
这样,加载的 cookies 将包含用户的登录凭据,使得页面可以继续保持登录状态。
使用 Page.LocalStorage 属性保存和加载本地存储数据:
某些网站使用本地存储(如 localStorage)来保存用户的登录状态或其他相关数据。你可以使用 Page.EvaluateFunctionAsync() 方法来获取和设置本地存储数据。例如:
// 保存本地存储数据
var localStorageData = await page.EvaluateFunctionAsync<string>("() => JSON.stringify(localStorage)");
// 将 localStorageData 保存到文件或数据库中
// 加载本地存储数据
// var savedLocalStorageData = LoadLocalStorageDataFromStorage();
await page.EvaluateExpressionAsync($"localStorage.setItem('data', {savedLocalStorageData})");
通过获取和设置本地存储数据,你可以在后续的会话中恢复用户的登录状态。
保存登录状态的示例:
using PuppeteerSharp;
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
var options = new LaunchOptions
{
Headless = false // 可见浏览器,方便演示
};
using (var browser = await Puppeteer.LaunchAsync(options))
using (var page = await browser.NewPageAsync())
{
await page.GoToAsync("https://example.com/login");
// 在这里执行登录操作,并等待登录成功后的页面加载完成
// 保存登录状态(cookies)
var cookies = await page.GetCookiesAsync();
// 将 cookies 保存到文件或数据库中
}
}
}
在上述示例中,我们使用 page.GetCookiesAsync() 方法获取登录后的页面的 cookies,并将其保存到变量 cookies 中。你可以根据需要将 cookies 保存到文件或数据库中,以便后续使用。
使用登录状态的示例:
using PuppeteerSharp;
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
var options = new LaunchOptions
{
Headless = false // 可见浏览器,方便演示
};
using (var browser = await Puppeteer.LaunchAsync(options))
using (var page = await browser.NewPageAsync())
{
// 加载之前保存的登录状态(cookies)
var savedCookies = LoadCookiesFromStorage(); // 从文件或数据库中加载保存的 cookies
await page.SetCookiesAsync(savedCookies);
// 使用登录状态浏览其他页面
await page.GoToAsync("https://example.com/profile"); // 这里可以是需要登录才能访问的页面
// 在这里执行需要登录状态的操作
// ...
// 保存更新后的登录状态(可选)
var updatedCookies = await page.GetCookiesAsync();
// 将 updatedCookies 保存到文件或数据库中
}
}
static CookieParam[] LoadCookiesFromStorage()
{
// 从文件或数据库中加载保存的 cookies 并返回 CookieParam[] 数组
// 示例中使用自定义的 CookieParam 类型,你可以根据实际情况调整
// ...
}
}
在上述示例中,我们使用 page.SetCookiesAsync() 方法将之前保存的登录状态(cookies)加载到新的页面中。然后,我们可以使用这个已登录的页面执行需要登录状态的操作。如果有必要,你还可以使用 page.GetCookiesAsync() 方法获取更新后的 cookies,并将其保存到文件或数据库中。
请注意,示例中的 LoadCookiesFromStorage() 方法是一个占位方法,你需要根据实际情况实现加载保存的 cookies 的逻辑。
在 PuppeteerSharp 中,你可以通过监听 Page.Request 事件来拦截和处理 AJAX 请求。下面是一个示例代码,演示了如何拦截 AJAX 请求并对其进行处理:
using PuppeteerSharp;
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
var options = new LaunchOptions
{
Headless = false // 可见浏览器,方便演示
};
using (var browser = await Puppeteer.LaunchAsync(options))
using (var page = await browser.NewPageAsync())
{
// 监听请求事件
page.Request += async (sender, e) =>
{
// 判断是否为 AJAX 请求
if (e.Request.ResourceType == ResourceType.Xhr)
{
// 输出请求的 URL
Console.WriteLine($"Intercepted AJAX Request: {e.Request.Url}");
// 拦截并修改请求的数据,这里可以根据需求进行处理
// 例如,修改请求的 Headers 或 PostData
await e.Request.ContinueAsync(new Payload
{
Headers = e.Request.Headers,
Method = e.Request.Method,
PostData = e.Request.PostData
});
}
else
{
// 对于非 AJAX 请求,可以选择继续请求或中止请求
await e.Request.ContinueAsync();
}
};
await page.GoToAsync("https://example.com");
// 在页面中触发 AJAX 请求
await page.EvaluateExpressionAsync(@"fetch('https://api.example.com/data', { method: 'GET' })");
// 等待一段时间,以便观察和处理拦截的 AJAX 请求
// 关闭浏览器
await browser.CloseAsync();
}
}
}
在上述示例中,我们监听了 Page.Request 事件,并在事件处理程序中判断请求的 ResourceType 是否为 ResourceType.Xhr,即 AJAX 请求。如果是 AJAX 请求,我们输出请求的 URL,并可以根据需求拦截并修改请求的数据,然后使用 e.Request.ContinueAsync() 方法继续请求。
对于非 AJAX 请求,你可以根据需要选择继续或中止请求,并使用 e.Request.ContinueAsync() 方法进行相应的操作。
请注意,拦截 AJAX 请求可能会对页面的功能和性能产生影响,因此在使用时需要谨慎考虑。确保你的拦截逻辑正确处理请求,并避免对页面的正常运行造成不必要的干扰。
AForge.NET(资源框架)
AForge库广泛用于计算机视觉和人工智能的应用领域,涵盖图像处理、视频处理、机器学习、人脸识别、手写数字识别、物体识别等方面。具体应用场景包括:
github源码地址:https://github.com/andrewkirillov/AForge.NET
图像去噪、图像增强、图像合并、图像分割等图像处理操作。
视频录制、视频采集、视频加速、视频滤镜、视频分类等视频处理操作。
目标检测、目标跟踪、人脸检测和识别、指纹识别等计算机视觉操作。
基于神经网络的图像识别、文本分类、情感分析、自然语言处理等机器学习操作。
AForge库的优点和缺点
优点
AForge库是一个开源的、跨平台的计算机视觉和人工智能库,具有广泛的应用领域和强大的扩展性。
AForge库提供了丰富的图像处理和视频处理算法、机器学习和神经网络模型,并且具有高效、易用、稳定的特点。
AForge库的文档和示例非常详细,易于理解和使用。
AForge库提供了免费的开源许可证,可以在商业和非商业项目中免费使用。
缺点
AForge库的文档和教程缺乏中文版本,不便于国内开发者使用和学习。
AForge库的API设计较为简单,没有过多的抽象和封装,可能会导致一定程度上的代码冗余和重复。
AForge库虽然提供了多种图像处理和视频处理算法,但是在某些复杂场景下可能需要自行开发特定算法。
图像处理模块案例介绍
using AForge;
using AForge.Imaging.Filters;
// 创建滤镜对象
FiltersSequence filter = new FiltersSequence();
filter.Add(new Grayscale(0.2125, 0.7154, 0.0721));
filter.Add(new Threshold(128));
// 加载图像
Bitmap image = new Bitmap("test.jpg");
// 应用滤镜
image = filter.Apply(image);
// 保存图像
image.Save("result.jpg");
视频处理模块案例介绍
using AForge.Video;
using AForge.Video.DirectShow;
// 创建摄像头对象
FilterInfoCollection videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
VideoCaptureDevice camera = new VideoCaptureDevice(videoDevices[0].MonikerString);
// 指定视频大小和帧率
camera.VideoResolution = camera.VideoCapabilities[0];
camera.DesiredFrameRate = 30;
// 开始采集
camera.Start();
// 定义帧处理事件
void ProcessFrame(object sender, NewFrameEventArgs eventArgs)
{
// 获取当前帧
Bitmap frame = (Bitmap)eventArgs.Frame.Clone();
// 在帧上绘制一个圆形
Graphics g = Graphics.FromImage(frame);
Pen pen = new Pen(Color.Red, 5);
g.DrawEllipse(pen, 100, 100, 200, 200);
// 显示帧
pictureBox1.Image = frame;
}
// 挂载帧处理事件
camera.NewFrame += new NewFrameEventHandler(ProcessFrame);
// 停止采集
camera.Stop();
人脸识别模块案例介绍
using AForge;
using AForge.Video;
using AForge.Video.DirectShow;
using AForge.Imaging;
using AForge.Imaging.Filters;
using System.Drawing;
// 创建摄像头对象
FilterInfoCollection videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
VideoCaptureDevice camera = new VideoCaptureDevice(videoDevices[0].MonikerString);
// 选择分辨率并开始采集视频流
camera.VideoResolution = camera.VideoCapabilities[0];
camera.NewFrame += new NewFrameEventHandler(video_NewFrame);
camera.Start();
// 声明人脸检测器
HaarObjectDetector detector = new HaarObjectDetector(new HaarCascade("haarcascade_frontalface_default.xml"));
// 视频流处理函数
void video_NewFrame(object sender, NewFrameEventArgs eventArgs)
{
Bitmap bitmap = (Bitmap)eventArgs.Frame.Clone();
// 转换图片为灰度图
Grayscale grayfilter = new Grayscale(0.2125, 0.7154, 0.0721);
Bitmap grayImage = grayfilter.Apply(bitmap);
// 检测人脸
Rectangle[] rectangles = detector.ProcessFrame(grayImage);
// 高亮标记所有检测到的人脸
if (rectangles.Length > 0)
{
using (Graphics g = Graphics.FromImage(bitmap))
{
Pen pen = new Pen(Color.Red, 2);
foreach (Rectangle rectangle in rectangles)
{
g.DrawRectangle(pen, rectangle);
}
}
}
pictureBox1.Image = bitmap;
}
以上代码中,使用AForge.Video.DirectShow命名空间的VideoCaptureDevice类来从本地摄像头捕获视频帧,通过调用video_NewFrame函数对每一帧图像进行处理。在视频中查找人脸时,我们使用了HaarObjectDetector类,该类使用一系列预定义的Haar特征进行人脸检测,并返回包含所有检测到的人脸的矩形数组。这些矩形可以用于在图像上高亮标记所有检测到的人脸,以进行识别。在这个代码示例中,HaarObjectDetector类使用了人脸检测器,其构造函数中传入了一个名为"
haarcascade_frontalface_default.xml"的文件。这个文件是OpenCV中已经训练好的、用于人脸检测的Haar特征分类器文件,可以通过以下方式获得:
在OpenCV官网下载:
Haar特征分类器文件可以在OpenCV官网中找到下载链接。您需要找到适合您当前使用的版本的特征分类器文件(如2.4版本),然后从OpenCV的源代码中提取出来。在下载并编译OpenCV后,您可以在源代码目录的"build\etc\haarcascades"子目录中找到这些文件。
使用现有的GitHub资源:
除了在OpenCV官网上找到的特征分类器文件外,还可以在GitHub上找到其他资源。例如,对于人脸检测,您可以从@opencv库中找到不同规模和角度的haar特征分类器文件。
训练自己的分类器:
如果现有的分类器文件不能满足您的需求,您也可以通过训练自己的分类器来实现更精确的人脸检测。这需要的时间和资源比较大,并且需要一定的计算机视觉和机器学习基础。通常,您需要准备正面人脸的大量样本图像和负面(非人脸)图像,并使用OpenCV提供的工具来训练分类器。训练好的分类器可以保存为XML文件,然后在您的代码中使用。
总结AForge库
AForge库作为.NET平台下的计算机视觉和人工智能库,具有高效、易用、稳定等特点,提供了丰富的图像处理和视频处理算法、机器学习和神经网络模型。它的使用场景广泛,可以应用于图像处理、视频处理、目标检测和识别、机器学习等领域。同时,它也存在一些缺点,如文档教程不够完善、API设计简单等。总的来说,AForge库是.NET平台下非常不错的一款计算机视觉和人工智能库,开发者可以根据自己的实际需求选择合适的组件模块进行开发。
官方网站:https://www.aforgenet.com/framework/。
Flaui使用说明
简介
看Flaui源码里的例子代码如下图:
FlaUI 是一个 .NET 库,可帮助对 Windows 应用程序(Win32、WinForms、WPF、Store Apps 等)进行自动化 UI 测试。
它基于 Microsoft 的本机 UI 自动化库,因此是它们的包装器。
FlaUI 包装了 UI Automation 库中的几乎所有内容,但也提供了本机对象,以防有人有 FlaUI 尚未涵盖的特殊需求。
源代码网址:http://www.github.com/Roemer/FlaUI
FlaUI源代码文档:https://github.com/FlaUI/FlaUI/wiki
FlaUI官方介绍讲分两个版本
UIA2:原生 UI 自动化 API 的托管库
UIA2 是只管理的,这对 C# 来说很好,但它不支持更新的功能(如触摸),而且它也不能很好地与 WPF 一起工作,甚至更糟糕的是与 Windows 应用商店应用程序一起工作。
UIA3:原生 UI 自动化 API 的 Com 库
UIA3 是最新的,非常适合 WPF/Windows 商店应用程序,但不幸的是,它可能有一些 WinForms 应用程序的错误(请参阅常见问题解答),这些错误在 UIA2 中不存在。
尝试下来:winform界面 尽量用UIA2,其他用UIA3,并且同一APPDomain只能存在一个对象,也切换时需要重启。
一、获取窗体
获取窗体有时候最困难,你可能遇到且不限于:类名重复、类名变化、窗体名重复、窗体父子关系但是父窗体设置成MDI,甚至有的窗体实际上是个Panle等等诸如此类。
获取窗体分为四种模式:
为什么要分为四种模式呢?写RPA脚本的时候要根据窗体的层级关系选择合适的代码,查找的执行效率和查询的范围有关,预设的范围越小,查询的效率越快。这几种模式下边会一一介绍。
[Description("当前进程主窗体")]
ByMain,
[Description("当前进程主窗体的子窗体")]
ByMainChild,
[Description("当前进程所有弹出窗体")]
ByAllTopLevel,
[Description("当前桌面所有弹出窗体")]
ByAllDesktop,
获取的条件:
获取的条件也要根据实际情况进行选择,有些窗体类名一致、窗体名不同,有些类名窗体名都不同。值的注意的是XPath组件内部自己生成的类似xml结构的固定格式,执行效率很高,建议获取元素的时候使用。
[Description("类名")]
ClassName,
[Description("窗体名")]
Title,
[Description("AutomationId")]
AutomationId,
[Description("XPath(窗体不可用)")]
XPath,
[Description("类名和窗体名")]
ClassNameAndTitle,
[Description("类名或窗体名")]
ClassNameOrTitle,
1.ByMain
要捕获的窗体只有一个、或者有父子关系时的父窗体(模态框)。
注意:非模态框不能用此模式
System.Diagnostics.Process[] processes = System.Diagnostics.Process.GetProcessesByName(processName);//获取进程
var id = processes.First().Id;
var app = FlaUI.Core.Application.Attach(id);
//new TimeSpan(1) 不能缺省 否则会假死
var mainWindow = appl?.GetMainWindow(AutomationBase, new TimeSpan(1));
2.ByMainChild
要捕获的窗体是父子关系时的子窗体(模态框)
System.Diagnostics.Process[] processes = System.Diagnostics.Process.GetProcessesByName(processName);//获取进程
var id = processes.First().Id;
var app = FlaUI.Core.Application.Attach(id);
//new TimeSpan(1) 不能缺省 否则会假死
var mainWindow = appl?.GetMainWindow(AutomationBase, new TimeSpan(1));
var property = model.WindowProperty;
var controlType = model.ControlType;
var frameworkType = model.FrameworkType;
var className = model.ClassName;
var name = model.Title;
var automationId = model.AutomationId;
mainWindows = FindWindowByAllChildren(mainWindow, property, controlType, frameworkType, className, name, automationId);
private static AutomationElement[] FindWindowByAllChildren(AutomationElement windows,
PropertyType property, ControlType controlType, FrameworkType frameworkType,
string className, string name, string automationId)
{
AutomationElement[] mainWindows = null;
switch (property)
{
case PropertyType.ClassName:
{
//桌面的直接子窗口
mainWindows = windows?.FindAllChildren(cf => cf.ByClassName(className).
And(cf.ByControlType(controlType).And(cf.ByFrameworkType(frameworkType))));
break;
}
case PropertyType.Title:
{
//桌面的直接子窗口
mainWindows = windows?.FindAllChildren(cf => cf.ByName(name).
And(cf.ByControlType(controlType).And(cf.ByFrameworkType(frameworkType))));
break;
}
case PropertyType.AutomationId:
{
//桌面的直接子窗口
mainWindows = windows?.FindAllChildren(cf => cf.ByAutomationId(automationId).
And(cf.ByControlType(controlType).And(cf.ByFrameworkType(frameworkType))));
break;
}
case PropertyType.ClassNameAndTitle:
{
//桌面的直接子窗口
mainWindows = windows?.FindAllChildren(cf => cf.ByClassName(className).And(cf.ByName(name)).
And(cf.ByControlType(controlType).And(cf.ByFrameworkType(frameworkType))));
break;
}
case PropertyType.ClassNameOrTitle:
{
//桌面的直接子窗口
mainWindows = windows?.FindAllChildren(cf => cf.ByClassName(className).Or(cf.ByName(name)).
And(cf.ByControlType(controlType).And(cf.ByFrameworkType(frameworkType))));
break;
}
default:
break;
}
return mainWindows;
}
3.ByAllTopLevel
当一个进程会弹出多个窗体并且要捕获的窗体是非模态框(和主窗体同级)
System.Diagnostics.Process[] processes = System.Diagnostics.Process.GetProcessesByName(processName);//获取进程
var id = processes.First().Id;
var app = FlaUI.Core.Application.Attach(id);
//new TimeSpan(1) 不能缺省 否则会假死
var windows = appl?.GetAllTopLevelWindows(AutomationBase).ToList();
//下边又分两种模式一种是
//主窗体A 窗体B和主窗体A同级 需要的是窗体B此时走isheet =0
//主窗体A 窗体B和主窗体A同级 窗体C是窗体B的子窗体(模态)isheet>0
//此处应该循环
var property = model.WindowProperty;
var controlType = model.ControlType;
var frameworkType = model.FrameworkType;
var className = model.ClassName;
var name = model.Title;
var automationId = model.AutomationId;
if (isheet == 0)
{
mainWindows = FindWindowByAllTopLevelWindows(windows, property, className, name, automationId)?.ToList();
}
else
{
mainWindows = FindWindowByAllChildren(mainWindow, property, controlType, frameworkType, className, name, automationId).ToList();
}
if (mainWindows?.Count() != 1)
{
var txt = $"{className},{name},{automationId}";
msg = $"第{isheet}层\r\n{GetDescription(windowtype)}|通过条件:{GetDescription(property)}值:{txt}\r\n查询到窗体个数{mainWindows.Count()}不唯一," +
$"请尝试切换其他模式重试," +
$"重试全部失败时,需定制插件";
break;
}
mainWindow = mainWindows?.FirstOrDefault();
private static AutomationElement[] FindWindowByAllTopLevelWindows(List<Window> windows,
PropertyType property,
string className, string name, string automationId)
{
List<Window> mainWindows = null;
//主窗体和子窗体同级时
//简单方式(常用)
switch (property)
{
case PropertyType.ClassName:
{
//桌面的直接子窗口
mainWindows = windows?.FindAll(cf => cf.ClassName.Equals(className));
break;
}
case PropertyType.Title:
{
//桌面的直接子窗口
mainWindows = windows?.FindAll(cf => cf.Name.Equals(name));
break;
}
case PropertyType.AutomationId:
{
//桌面的直接子窗口
mainWindows = windows?.FindAll(cf => cf.AutomationId.Equals(automationId));
break;
}
case PropertyType.ClassNameAndTitle:
{
//桌面的直接子窗口
mainWindows = windows?.FindAll(cf => cf.ClassName.Equals(className) && cf.Name.Equals(name));
break;
}
case PropertyType.ClassNameOrTitle:
{
//桌面的直接子窗口
mainWindows = windows?.FindAll(cf => cf.ClassName.Equals(className) || cf.Name.Equals(name));
break;
}
default:
break;
}
return mainWindows?.ToArray();
}
private static AutomationElement[] FindWindowByAllChildren(AutomationElement windows,
PropertyType property, ControlType controlType, FrameworkType frameworkType,
string className, string name, string automationId)
{
AutomationElement[] mainWindows = null;
switch (property)
{
case PropertyType.ClassName:
{
//桌面的直接子窗口
mainWindows = windows?.FindAllChildren(cf => cf.ByClassName(className).
And(cf.ByControlType(controlType).And(cf.ByFrameworkType(frameworkType))));
break;
}
case PropertyType.Title:
{
//桌面的直接子窗口
mainWindows = windows?.FindAllChildren(cf => cf.ByName(name).
And(cf.ByControlType(controlType).And(cf.ByFrameworkType(frameworkType))));
break;
}
case PropertyType.AutomationId:
{
//桌面的直接子窗口
mainWindows = windows?.FindAllChildren(cf => cf.ByAutomationId(automationId).
And(cf.ByControlType(controlType).And(cf.ByFrameworkType(frameworkType))));
break;
}
case PropertyType.ClassNameAndTitle:
{
//桌面的直接子窗口
mainWindows = windows?.FindAllChildren(cf => cf.ByClassName(className).And(cf.ByName(name)).
And(cf.ByControlType(controlType).And(cf.ByFrameworkType(frameworkType))));
break;
}
case PropertyType.ClassNameOrTitle:
{
//桌面的直接子窗口
mainWindows = windows?.FindAllChildren(cf => cf.ByClassName(className).Or(cf.ByName(name)).
And(cf.ByControlType(controlType).And(cf.ByFrameworkType(frameworkType))));
break;
}
default:
break;
}
return mainWindows;
}
4、ByAllDesktop
效率最慢,当进程窗体很多时巨慢我本机试的1-3s
var property = model.WindowProperty;
var controlType = model.ControlType;
var frameworkType = model.FrameworkType;
var className = model.ClassName;
var name = model.Title;
var automationId = model.AutomationId;
mainWindows = FindWindowByAllChildren(windows, property, controlType, frameworkType, className, name, automationId);
windows = mainWindows?.FirstOrDefault()?.AsWindow();
private static AutomationElement[] FindWindowByAllChildren(AutomationElement windows,
PropertyType property, ControlType controlType, FrameworkType frameworkType,
string className, string name, string automationId)
{
AutomationElement[] mainWindows = null;
switch (property)
{
case PropertyType.ClassName:
{
//桌面的直接子窗口
mainWindows = windows?.FindAllChildren(cf => cf.ByClassName(className).
And(cf.ByControlType(controlType).And(cf.ByFrameworkType(frameworkType))));
break;
}
case PropertyType.Title:
{
//桌面的直接子窗口
mainWindows = windows?.FindAllChildren(cf => cf.ByName(name).
And(cf.ByControlType(controlType).And(cf.ByFrameworkType(frameworkType))));
break;
}
case PropertyType.AutomationId:
{
//桌面的直接子窗口
mainWindows = windows?.FindAllChildren(cf => cf.ByAutomationId(automationId).
And(cf.ByControlType(controlType).And(cf.ByFrameworkType(frameworkType))));
break;
}
case PropertyType.ClassNameAndTitle:
{
//桌面的直接子窗口
mainWindows = windows?.FindAllChildren(cf => cf.ByClassName(className).And(cf.ByName(name)).
And(cf.ByControlType(controlType).And(cf.ByFrameworkType(frameworkType))));
break;
}
case PropertyType.ClassNameOrTitle:
{
//桌面的直接子窗口
mainWindows = windows?.FindAllChildren(cf => cf.ByClassName(className).Or(cf.ByName(name)).
And(cf.ByControlType(controlType).And(cf.ByFrameworkType(frameworkType))));
break;
}
default:
break;
}
return mainWindows;
}
二、获取元素
1.通过Xpath获取 推荐使用
类似于文件路径,这是个相对路径,可以稍微理解一下,下付转换代码
//通过XPath获取
var childrens = window?.FindAllByXPath(xPath);
//转化Xpath
public static string TranslationXpath(string xpath)
{
if (!xpath.ToLower().Contains("window"))
{
return xpath;
}
var eles = xpath.Split(new string[] { "/" }, StringSplitOptions.RemoveEmptyEntries);
var split = "/";
var res = "";
for (int i = 1; i < eles.Length; i++)
{
res += split + eles[i];
}
return res;
}
//通过类名窗体名查找
switch (property)
{
case PropertyType.ClassName:
{
if (controlType == ControlType.Unknown)
{
elements = window?.FindAllDescendants(cf => cf.ByClassName(value[0]));
}
else
{
elements = window?.FindAllDescendants(cf => cf.ByClassName(value[0]).And(cf.ByControlType(controlType)));
}
break;
}
case PropertyType.Title:
{
if (controlType == ControlType.Unknown)
{
elements = window?.FindAllDescendants(cf => cf.ByName(value[1]));
}
else
{
elements = window?.FindAllDescendants(cf => cf.ByName(value[1]).And(cf.ByControlType(controlType)));
}
break;
}
case PropertyType.AutomationId:
{
if (controlType == ControlType.Unknown)
{
elements = window?.FindAllDescendants(cf => cf.ByAutomationId(value[2]));
}
else
{
elements = window?.FindAllDescendants(cf => cf.ByAutomationId(value[2]).And(cf.ByControlType(controlType)));
}
break;
}
case PropertyType.ClassNameAndTitle:
{
if (controlType == ControlType.Unknown)
{
elements = window?.FindAllDescendants(cf => cf.ByClassName(value[0]).And(cf.ByName(value[1])));
}
else
{
elements = window?.FindAllDescendants(cf => cf.ByClassName(value[0]).And(cf.ByName(value[1])).And(cf.ByControlType(controlType)));
}
break;
}
case PropertyType.ClassNameOrTitle:
{
if (controlType == ControlType.Unknown)
{
elements = window?.FindAllDescendants(cf => cf.ByClassName(value[0]).Or(cf.ByName(value[1])));
}
else
{
elements = window?.FindAllDescendants(cf => cf.ByClassName(value[0]).Or(cf.ByName(value[1])).And(cf.ByControlType(controlType)));
}
break;
}
default:
break;
}
三、元素操作
我现在只用到了textbox、combox、checkbox、RadioButton之类的简单的,复杂的可以自行参照文档,注意 有时候winform和wpf或者其他win32的界面同样的控件可能发送的写法不同,需要定制,我这里定义了个插件来定制化不能使用的界面,有需要的可以自行去除
public static void SendText(string key, AutomationElement element, string txt)
{
try
{
var appl = GetApplication(key);
var pElement = appl?.AutomationComponent?.TextBox(element, txt);
if (!(pElement.HasValue && pElement.Value))
{
var textbox = element?.AsTextBox();
if (textbox != null)
{
textbox.Text = txt;
//发送失败时尝试用粘贴板粘贴
if (string.IsNullOrEmpty(textbox.Text))
textbox.Enter(txt);
}
}
}
catch (Exception ex) { throw ex; }
}
public static void ComboBoxSelectText(string key, AutomationElement element, string txt)
{
try
{
var appl = GetApplication(key);
var pElement = appl?.AutomationComponent?.ComboBox(element, txt);
if (!(pElement.HasValue && pElement.Value))
{
var combox = element?.AsComboBox();
if (combox != null)
{
combox.Select(txt);
combox.Collapse();
}
}
}
catch (Exception ex) { throw ex; }
}
public static void ComboBoxSelectIndex(string key, AutomationElement element, int index)
{
try
{
var appl = GetApplication(key);
var pElement = appl?.AutomationComponent?.ComboBox(element, index);
if (!(pElement.HasValue && pElement.Value))
{
var combox = element?.AsComboBox();
if (combox != null)
{
try
{
combox.Select(index);
combox.Collapse();
}
catch (Exception ex)
{
throw ex;
}
}
}
}
catch (Exception ex) { throw ex; }
}
public static void CheckBoxChecked(string key, AutomationElement element, bool isCheck)
{
try
{
var appl = GetApplication(key);
var pElement = appl?.AutomationComponent?.CheckBox(element, isCheck);
if (!(pElement.HasValue && pElement.Value))
{
var check = element?.AsCheckBox();
if (check != null)
check.IsChecked = isCheck;
}
}
catch (Exception ex) { throw ex; }
}
public static void RadioButtonCheck(string key, AutomationElement element, bool isCheck)
{
try
{
var appl = GetApplication(key);
var pElement = appl?.AutomationComponent?.RadioButton(element, isCheck);
if (!(pElement.HasValue && pElement.Value))
{
var radioButton = element?.AsRadioButton();
if (radioButton != null)
radioButton.IsChecked = isCheck;
}
}
catch (Exception ex) { throw ex; }
}
public static void RadioButtonClick(string key, AutomationElement element)
{
try
{
var appl = GetApplication(key);
var pElement = appl?.AutomationComponent?.RadioButton(element);
if (!(pElement.HasValue && pElement.Value))
{
var radioButton = element?.AsRadioButton();
if (radioButton != null)
radioButton.Click();
}
}
catch (Exception ex) { throw ex; }
}
public static void DateTimePicker(string key, AutomationElement element, DateTime dateTime)
{
try
{
var appl = GetApplication(key);
var pElement = appl?.AutomationComponent?.DateTimePicker(element, dateTime);
if (!(pElement.HasValue && pElement.Value))
{
var picker = element?.AsDateTimePicker();
if (picker != null)
picker.SelectedDate = dateTime;
}
}
catch (Exception ex) { throw ex; }
}
public static void Calendar(string key, AutomationElement element, DateTime dateTime)
{
try
{
var appl = GetApplication(key);
var pElement = appl?.AutomationComponent?.Calendar(element, dateTime);
if (!(pElement.HasValue && pElement.Value))
{
var calendar = element?.AsCalendar();
if (calendar != null)
calendar.SelectDate(dateTime);
}
}
catch (Exception ex) { throw ex; }
}
public static void Spinner(string key, AutomationElement element, double value)
{
try
{
var appl = GetApplication(key);
var pElement = appl?.AutomationComponent?.Spinner(element, value);
if (!(pElement.HasValue && pElement.Value))
{
var spinner = element?.AsSpinner();
if (spinner != null)
spinner.Value = value;
}
}
catch (Exception ex) { throw ex; }
}
public static void Slider(string key, AutomationElement element, int value)
{
try
{
var appl = GetApplication(key);
var pElement = appl?.AutomationComponent?.Slider(element, value);
if (!(pElement.HasValue && pElement.Value))
{
var slider = element?.AsSlider();
if (slider != null)
slider.Value = AdjustNumberIfOnlyValue(slider, value);
}
}
catch (Exception ex) { throw ex; }
}
public static void ButtonClick(string key, AutomationElement element)
{
try
{
var appl = GetApplication(key);
var pElement = appl?.AutomationComponent?.Button(element);
if (!(pElement.HasValue && pElement.Value))
{
var button = element?.AsButton();
if (button != null)
button.Invoke();
}
}
catch (Exception ex) { throw ex; }
}
public static void ListSelete()
{
}
private static double AdjustNumberIfOnlyValue(Slider slider, double number)
{
if (slider.IsOnlyValue)
{
return number * 10;
}
return number;
}
先写这么多吧,后边有空写下类似Spy++的工具的坑。。。
点击下载例子代码VS2022:
影刀采用类似技术
我们接下来的操作的对象是微信窗体,分成这几个步骤可以将微信窗体进行自动化操作前的初始化
(1)通过窗体名称找到微信句柄指针。
(2)通过窗体指针找到微信的进程ID。
(3)使用进程ID初始化自动化组件服务。
(4)设置微信窗体的状态为激活。
(1)找到PC端微信窗体并获取微信窗体的句柄数据
我们借助WINDOWS的两个API函数 ,先定义好API的C#调用方式。
//根据名称获取窗体句柄
[DllImport("user32.dll", EntryPoint = "FindWindow")]
private extern static IntPtr FindWindow(string lpClassName, string lpWindowName);
//根据句柄获取进程ID
[DllImport("User32.dll", CharSet = CharSet.Auto)]
public static extern int GetWindowThreadProcessId(IntPtr hwnd, out int ID);
点击并拖拽以移动
(2)通过以下代码获取到微信的窗口句柄和进程ID
int weChatID = 0;
IntPtr hwnd = FindWindow(null, "微信");
if (hwnd != IntPtr.Zero)
{
GetWindowThreadProcessId(hwnd, out weChatID);
}
点击并拖拽以移动
如果找到了微信句柄那么就可以继续了,如果没有那么请扫描登录或者进行其他的操作。比如自动打开微信。
(3)使用FlaUI.Core组件根据进程ID初始化自动化组件
//根据微信进程ID绑定FLAUI
var application = FlaUI.Core.Application.Attach(weChatID);
var automation = new UIA3Automation();
//获取微信window自动化操作对象
var Window = application.GetMainWindow(automation);
点击并拖拽以移动
自动化FlaUI对象构造就是通过步骤一获取微信进程ID进行构造。
(4)如果用户将微信最小化,我们需要将微信窗体置顶激活或者最大化
public void Focus()
{
if (window.AsWindow().Patterns.Window.PatternOrDefault != null)
{
//将微信窗体设置为默认焦点状态
window.AsWindow().Patterns.Window.Pattern.SetWindowVisualState(FlaUI.Core.Definitions.WindowVisualState.Normal);
}
}
点击并拖拽以移动
这个方法可以将微信窗体设置为活动焦点状态。
进行微信自动化前,因为采集或者发送消息任务执行需要时间,并且微信窗体会将焦点长期占有,导致我们对软件失去控制,所以我们需要使用热键的方式将任务停止。我们这里采用热键是点击并拖拽以移动编辑。
(1)编写一个热键管理类
这个类定义了捕获热键消息的ID,注册热键,注销热键的功能。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace OnlineRetailers.Extension.Page.UIAuto.Business
{
/// <summary>
/// 热键管理
/// </summary>
public class WinHotKey
{
/// <summary>
/// 注册热键
/// </summary>
/// <param name="hWnd">为窗口句柄</param>
/// <param name="id">注册的热键识别ID</param>
/// <param name="control">组合键代码 Alt的值为1,Ctrl的值为2,Shift的值为4,Shift+Alt组合键为5
/// Shift+Alt+Ctrl组合键为7,Windows键的值为8
/// </param>
/// <param name="vk">按键枚举</param>
/// <returns></returns>
[DllImport("user32")]
static extern bool RegisterHotKey(IntPtr hWnd, int id, uint control, Keys vk);
/// <summary>
/// 取消注册的热键
/// </summary>
/// <param name="hWnd">窗口句柄</param>
/// <param name="id">注册的热键id</param>
/// <returns></returns>
[DllImport("user32")]
static extern bool UnregisterHotKey(IntPtr hWnd, int id);
/// <summary>
/// 任务停止热键
/// </summary>
/// <param name="Handle"></param>
public static void RegisterStop(IntPtr Handle)
{
WinHotKey.RegisterHotKey(Handle, StopId, 2, Keys.F8);
}
/// <summary>
/// 取消停止热键
/// </summary>
/// <param name="Handle"></param>
public static void UnRegisterStop(IntPtr Handle)
{
WinHotKey.UnregisterHotKey(Handle, StopId);
}
/// <summary>
/// 停止ID
/// </summary>
public static readonly int StopId = 8879;
}
}
点击并拖拽以移动
(2)窗体注册和注销热键
在窗体的Load事件中注册热键
this.Load += WXUIAuto_Load;
private void WXUIAuto_Load(object sender, EventArgs e)
{
WinHotKey.RegisterStop(this.Handle);
}
点击并拖拽以移动
在窗体关闭事件中注销事件
this.FormClosed += WXUIAuto_FormClosed;
private void WXUIAuto_FormClosed(object sender, FormClosedEventArgs e)
{
WinHotKey.UnRegisterStop(this.Handle);
}
点击并拖拽以移动
注册了热键事件后我们需要一个方法来监听热键的事件,From窗体中提供了处理消息WndProc方法,我们重写这个方法,并加入自己的逻辑。
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case 0x0312:
if (m.WParam.ToString().Equals(WinHotKey.StopId.ToString()))
{
//停止微信自动化任务
}
break;
}
base.WndProc(ref m);
}
点击并拖拽以移动
0x0312是热键的消息类型,消息参数是我们自己定义的,如果热键的消息参数等于停止的ID 8897就停止微信自动化任务。
接下来我们进行联系人采集学习
接下来我们如何采集到微信中的联系人列表呢?
(1)找到通讯录按钮UI元素执行点击操作。
(2)找到联系人面板并执行面板滚动操作
(3)滚动过程中获取面板List对象中的ListItem项中的各个子元素
(1)找到通讯录的按钮元素,发送点击事件将通讯录面板置为选中状态
点击并拖拽以移动编辑
通过UI元素路径查找到了通讯录UI对象,并执行点击事件。
private void ClickContacts()
{
//通过XPATH找到通讯录按钮
var ele = UI_WX_Window.Current.Find("/Pane[2]/Pane[1]/Button[3]");
//发送点击事件
UI_WX_Window.Current.ClickElement(ele);
}
点击并拖拽以移动
/Pane[2]/Pane[1]/Button[3]是XPath表达式,等于通讯录按钮在窗体中的路径。
然后发送点击事件,将联系人面板展示出来。
(2)当通讯录面板置为焦点后,微信的联系人信息全部在一个List滚动面板中。
我们通过XPATH方式找到联系人List面板的对象。
var list = UI_WX_Window.Current.Find("/Pane[2]/Pane[2]/Pane[2]/Pane/List");
点击并拖拽以移动
接下来我们通过以下代码获取当前可视区域的联系人
//获取当前可视区域的联系人
private void GetWXContact()
{
UI_WX_Window.Current.Focus();
var list = UI_WX_Window.Current.Find("/Pane[2]/Pane[2]/Pane[2]/Pane/List");
if (list != null)
{
//获取联系人面板中所有的子控件
var child = list.FindAllChildren();
//遍历控件数
foreach (var item in child)
{
var wxName = item.Name;
if (!Contacts.Contains(wxName) && wxName != "新的朋友" && wxName != "公众号")
{
Contacts.Add(wxName);
Log.WXLog.Current.Log(wxName);
}
}
}
}
点击并拖拽以移动
将联系人List面板中的子控件全部遍历出来并将ListItem中的联系人获取出来。
执行完代码后我们会发现如果我们的联系人面板出现滚动条后,遍历出来的控件只能获取到窗体视觉区域内的联系人。那么我们需要控制滚动条自动滚动,并循环调用GetWXContact()方法来获取联系人。
//获取联系人面板
var list = UI_WX_Window.Current.Find("/Pane[2]/Pane[2]/Pane[2]/Pane/List");
if (list != null)
{
//开启一个线程控制联系人滚动面板进行滚动
Thread th = new Thread(new ThreadStart(() =>
{
int i = 0;
while (true)
{
//获取滚动面板的视图
var contactScroll = list.Patterns.Scroll.Pattern;
//VerticalViewSize为当前可视区域在整个滚动面板滚动区域高度中的比例
var scroll = contactScroll.VerticalViewSize * i;
if (scroll > 0)
{
//如果滚动比例达到100%设置为1
scroll = 0;
}
//使用flaui组件将滚动面板的视图设置到滚动的位置
contactScroll.SetScrollPercent(1, scroll);
//滚动完成后在继续获取联系人
GetWXContact();
i++;
}
}));
th.Start();
}
点击并拖拽以移动
通过上述的代码就能将我们微信中的联系人和群信息全部采集出来!
鼠标按键及触摸操作
Mouse操作例子
// Directly jump to the position
Mouse.Position = new Point(10, 10);
// Slowly move the mouse to the desired position
Mouse.MoveBy(100, 100);
// Press and release the middle mouse button
Mouse.Click(MouseButton.Middle);
// Scroll 2 lines
Mouse.Scroll(2);
Keyboard按键操作
There are three kind of methods:
Press - Presses the desired key
Release - Releases the desired key
Type - Presses and releases the desired key
For input, there are also several types:
Scan-Code
Virtual Key-Code
Enum VirtualKeyShort
char
string
// Type A and then B
Keyboard.Type(VirtualKeyShort.KEY_A, VirtualKeyShort.KEY_B);
If you need to press them simultaneously, use the following:
// Press Alt and hold it, then type 1 and then release Alt
Keyboard.TypeSimultaneously(VirtualKeyShort.ALT, VirtualKeyShort.KEY_1);
If you do want to add additional logic while pressing a key, you can use the following:
// Press Alt and hold it, execute the custom logic, then release Alt
using (Keyboard.Pressing(VirtualKeyShort.ALT))
{
// Some custom logic
}
Touch触摸操作
Tap
Performs a tap at the given point(s).
var point = new Point(100, 100);
Touch.Tap(point);
Hold
Holds the given point(s) for a certain amount of time.
var point = new Point(100, 100);
Touch.Hold(TimeSpan.FromSeconds(2), point);
Drag
Drags the given point from the start to the end point.
var startPos = new Point(100, 100);
var endPos = Point.Add(startPos, new Size(100, 0));
Touch.Drag(TimeSpan.FromSeconds(2), startPos, endPos);
Transition
Generic drag version for multiple points where each point can have a different start and end point.
// Simultaneously moves one touch point down and the other sideways
var point1 = Tuple.Create(
new Point(100, 100),
new Point(100, 200)
);
var point2 = Tuple.Create(
new Point(100, 100),
new Point(200, 100)
);
Touch.Transition(TimeSpan.FromSeconds(2), point1, point2);
Pinch
Performs a two-finger pinch gesture.
var centerPoint = new Point(100, 100);
var startRadius = 0;
var endRadius = 100;
Touch.Pinch(centerPoint, startRadius, endRadius, TimeSpan.FromSeconds(2));
Rotate
Performs a two-finger rotate gesture where one finger is at the center and the second is rotated around it.
var centerPoint = new Point(100, 100);
var radius = 200;
var startAngle = 0;
var endAngle = 2 * Math.PI
Touch.Rotate(centerPoint, radius, startAngle, endAngle, TimeSpan.FromSeconds(3));
FlaUinspect获得WPF界面信息
FlaUI 如何获取页面的信息
打开这个FlaUinspect工具,工具的github地址:https://github.com/FlaUI/FlaUInspect/releases
压缩包包含源码和执行文件:
可以通过 以下看到 XPath地址
这个FlaUinspect项目是一个WPF项目,想深入研究的可以查看源码,跟踪调试一波。
这里主要的是可以通过以下两种方式来获取所需要的内容
第一种就像下面的一样,通过同一个页面独一无二的名字来获取到
var addressBook = mainWindow.FindFirstDescendant(cf => cf.ByName("聊天"));
第二种是这样的
可以通过 图上面的2的XPath地址来找到你想要的控件
var infoData = automationElement.FindAllByXPath("/Pane/Pane[1]");
总结
总的来说,这个技术还是很方便的,但是对于QQ这种底层是自绘技术的以及是使用QT,JAVA级的应用应该是实现不了。只能针对于微软的技术的产品WinFrom和WPF等。
大体来讲,还是降低了使用时候的难度的。
比如这个微信发送信息,你有功能了,你就可以自己扩展,比如,指定人发,群发,定时发,标签发送,实现完,对个人来讲,作用也是不错的。
RPA之基于FlaUI的微信发送消息给某人
使用步骤
1.引入Nuget包
Install-Package FlaUI.UIA3 -Version 3.2.0
2.实现一个简单的给指定人发送消息
代码如下(示例):
Process[] processes = Process.GetProcessesByName("WeChat");
if (processes.Count() != 1)
{
Console.WriteLine("微信未启动或启动多个微信");
}
else
{
//1.附加到微信进程
using (var app = Application.Attach(processes.First().Id))
{
using (var automation = new UIA3Automation())
{
//2.获取主界面
var mainWindow = app.GetMainWindow(automation);
Console.WriteLine("获取主界面");
//3.切换到通讯录
var elements = mainWindow.FindAll(FlaUI.Core.Definitions.TreeScope.Subtree, TrueCondition.Default);
var addressBook = mainWindow.FindFirstDescendant(cf => cf.ByName("通讯录"));
addressBook.DrawHighlight(System.Drawing.Color.Red);
var path = Debug.GetXPathToElement(addressBook);
Console.WriteLine("点击通讯录");
addressBook.Click();
//4.搜索
string target = "文件传输助手";
var searchTextBox = mainWindow.FindFirstDescendant(cf => cf.ByName("搜索")).AsTextBox();
searchTextBox.Click();
Keyboard.Type(target);
Keyboard.Type(VirtualKeyShort.RETURN);
Console.WriteLine("搜索目标对象");
//5.切换到对话框
Thread.Sleep(500);
var searchList = mainWindow.FindFirstDescendant(cf => cf.ByName("搜索结果"));
if (searchList != null)
{
var searchItem = searchList.FindAllDescendants().FirstOrDefault(cf => cf.Name == target && cf.ControlType == FlaUI.Core.Definitions.ControlType.ListItem);
searchItem?.DrawHighlight(System.Drawing.Color.Red);
searchItem?.AsListBoxItem().Click();
}
else
{
Console.WriteLine("没有搜索到内容");
}
Thread.Sleep(500);
//6.输入文本
string sendMsg = "这个是我微信的输入信息:" + DateTime.Now.ToString();
var msgInput = mainWindow.FindFirstDescendant(cf => cf.ByName("输入")).AsTextBox();
msgInput?.Click();
System.Windows.Forms.Clipboard.SetText(sendMsg);
Keyboard.TypeSimultaneously(new[] { VirtualKeyShort.CONTROL, VirtualKeyShort.KEY_V });
var sendBtn = mainWindow.FindFirstDescendant(cf => cf.ByName("sendBtn"));
sendBtn?.DrawHighlight(System.Drawing.Color.Red);
sendBtn?.Click();
}
}
}
代码有注释也容易理解。
就是搜索指定人,然后,发送指定信息给他。搞定。
3.实现一个获取会话列表批量发送消息
代码如下(示例):
Process[] processes = Process.GetProcessesByName("WeChat");
if (processes.Count() != 1)
{
Console.WriteLine("微信未启动或启动多个微信");
}
else
{
//1.附加到微信进程
using (var app = Application.Attach(processes.First().Id))
{
using (var automation = new UIA3Automation())
{
//2.获取主界面
var mainWindow = app.GetMainWindow(automation);
Console.WriteLine("获取主界面");
//3.切换到聊天目录
var elements = mainWindow.FindAll(TreeScope.Subtree, TrueCondition.Default);
var addressBook = mainWindow.FindFirstDescendant(cf => cf.ByName("聊天"));
addressBook.DrawHighlight(System.Drawing.Color.Red);
var path = Debug.GetXPathToElement(addressBook);
addressBook.Click();
Console.WriteLine("切换到聊天");
Thread.Sleep(2000);
//4.获取聊天列表
//只发前六个
var count = 0;
var searchTextBox = mainWindow.FindFirstDescendant(cf => cf.ByName("会话")).AsListBoxItem();
while (searchTextBox != null)
{
var list = searchTextBox.FindAllChildren();
foreach (var item in list)
{
count++;
var name = item.Name;
item.Click();
var type = item.ControlType;
item.DrawHighlight(System.Drawing.Color.Red);
var MsgSend = mainWindow.FindFirstDescendant(cf => cf.ByName("输入")).AsTextBox();
var MsgSendButton = mainWindow.FindFirstDescendant(cf => cf.ByName("sendBtn"));
if (MsgSend != null && MsgSendButton != null)
{
MsgSend.Click();
System.Windows.Forms.Clipboard.SetText($"群发消息,请忽略:{DateTime.Now}");
Keyboard.TypeSimultaneously(new[] { VirtualKeyShort.CONTROL, VirtualKeyShort.KEY_V });
MsgSendButton.Click();
Console.WriteLine($"发送信息:{name}");
Thread.Sleep(500);
}
if (count == 6)
{
break;
}
}
if (count == 6)
{
break;
}
for (int i = 0; i < list.Length; i++)
{
searchTextBox.Focus();
Keyboard.Press(VirtualKeyShort.DOWN);
Thread.Sleep(100);
}
searchTextBox = mainWindow.FindFirstDescendant(cf => cf.ByName("会话")).AsListBoxItem();
Thread.Sleep(2000);
}
}
}
}
这个代码重要是群发给了前6个人,如果会话没有发送按钮,就不会发送,避免影响更多人。
C#+FlaUI自动化+chatGPT实现微信AI问答
本文实现功能
本次主要介绍如何实现自动回复:
1、将文件传输助手置顶,模拟鼠标点击文件传输助手;
2、一直刷新会话列表,有新的消息就需要回复内容;
3、当刷新到新的消息时,模拟鼠标点击到对应的会话人,此时判断是群聊还是人,如果是群聊则不回复。
4、获取消息后转发给chatGPT,同时等待chatGPT回复内容。
5、获取chatGPT的内容后将内容输入到微信聊天框,并模拟鼠标点击发送按钮。
6、模拟鼠标点击文件传输助手,等待其它消息。
源码:
FlaUI学习总结
Find...XPath查找元素,参数如"//Button[@Name=\"聊天\"]",开头的\\表示模糊查询不分层级,Button为元素类型,后面Name为元素的Name属性
定位元素的局部搜索: .//Text; 全局搜索: //*/Text
查找元素
FindFirstChild
FindAllChildren
FindFirstDescendant
FindAllDescendants
var child = parent.FindFirst(TreeScope.Children, new PropertyCondition(Automation.PropertyLibrary.Element.AutomationIdProperty, "someId"));
var child = parent.FindFirstChild(ConditionFactory.ByAutomationId("someId"));
// Using the convenience method and the func notation
var child = parent.FindFirstChild(cf => cf.ByAutomationId("someId"));
The search conditions describe the "where" of the search. The following conditions are available:
Condition | Description |
---|---|
AndCondition | Used to put multiple conditions together with an and |
OrCondition | Used to put multiple conditions together with an or |
BoolCondition | Used for simple true and false conditions, usually not needed |
NotCondition | Used to negate a condition |
PropertyCondition | Used to search for specific properties like name or automation id |
Conditions can be either directly created or with the ConditionFactory
(available on any element).
以上条件都派生ConditionBase
如:
var andCondition1 = new AndCondition(
new PropertyCondition(automation.PropertyLibrary.Element.Name, "Exit"),
new PropertyCondition(automation.PropertyLibrary.Element.ControlType, ControlType.MenuItem));
var menuItem = window.FindFirstDescendant(andCondition1);
menuItem.AsMenuItem().Click();
var elements = Windows.FindAll(FlaUI.Core.Definitions.TreeScope.Subtree, TrueCondition.Default);
TrueConditon派生于BoolCondition
Searching by XPath
window.FindFirstByXPath($"/MenuBar/MenuItem[@Name='File']");
实例:
var process = Process.GetProcessesByName("Wechat").FirstOrDefault();
if (process != null)
{
ProcessId = process.Id;
}
try
{
var application = FlaUI.Core.Application.Attach(ProcessId);
var automation = new UIA3Automation();
//获取微信window自动化操作对象
wxWindow = application.GetMainWindow(automation);
}
catch (Exception ex)
{
if (MessageBox.Show(ex.Message, "异常", MessageBoxButtons.OK, MessageBoxIcon.Error) == DialogResult.OK)
this.Close();
}
//点击通讯录按钮
if (wxWindow == null)
{
return;
}
if (wxWindow.Patterns.Window.PatternOrDefault != null)
{
//将微信窗体设置为默认焦点状态
wxWindow.Patterns.Window.Pattern.SetWindowVisualState(FlaUI.Core.Definitions.WindowVisualState.Normal);
}
wxWindow.FindAllDescendants(x => x.ByControlType(FlaUI.Core.Definitions.ControlType.Button)).AsParallel()
.FirstOrDefault(item => item != null && item.Name == "通讯录")?.Click(false);
//点击聊天按钮
wxWindow.FindFirstByXPath("//Button[@Name=\"聊天\"]")?.Click(false);
滚动操作封装了一个User32.cs
public void ScrollEvent(int scroll)
{
INPUT[] inputs = new INPUT[1];
// 设置鼠标滚动事件
inputs[0].type = InputType.INPUT_MOUSE;
inputs[0].mi.dwFlags = MouseEventFlags.MOUSEEVENTF_WHEEL;
inputs[0].mi.mouseData = (uint)scroll;
// 发送输入事件
User32.SendInput(1, inputs, Marshal.SizeOf(typeof(INPUT)));
}
封装的类User32
internal static class User32
{
/// <summary>
/// 滚动条模拟
/// </summary>
/// <param name="nInputs"></param>
/// <param name="pInputs"></param>
/// <param name="cbSize"></param>
/// <returns></returns>
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
//根据名称获取窗体句柄
[DllImport("user32.dll", EntryPoint = "FindWindow")]
private extern static IntPtr FindWindow(string lpClassName, string lpWindowName);
//根据句柄获取进程ID
[DllImport("User32.dll", CharSet = CharSet.Auto)]
public static extern int GetWindowThreadProcessId(IntPtr hwnd, out int ID);
}
public struct INPUT
{
public InputType type;
public MouseInput mi;
}
// 输入类型
public enum InputType : uint
{
INPUT_MOUSE = 0x0000,
INPUT_KEYBOARD = 0x0001,
INPUT_HARDWARE = 0x0002
}
// 鼠标输入结构体
public struct MouseInput
{
public int dx;
public int dy;
public uint mouseData;
public MouseEventFlags dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
// 鼠标事件标志位
[Flags]
public enum MouseEventFlags : uint
{
MOUSEEVENTF_MOVE = 0x0001,
MOUSEEVENTF_LEFTDOWN = 0x0002,
MOUSEEVENTF_LEFTUP = 0x0004,
MOUSEEVENTF_RIGHTDOWN = 0x0008,
MOUSEEVENTF_RIGHTUP = 0x0010,
MOUSEEVENTF_MIDDLEDOWN = 0x0020,
MOUSEEVENTF_MIDDLEUP = 0x0040,
MOUSEEVENTF_XDOWN = 0x0080,
MOUSEEVENTF_XUP = 0x0100,
MOUSEEVENTF_WHEEL = 0x0800,
MOUSEEVENTF_HWHEEL = 0x1000,
MOUSEEVENTF_MOVE_NOCOALESCE = 0x2000,
MOUSEEVENTF_VIRTUALDESK = 0x4000,
MOUSEEVENTF_ABSOLUTE = 0x8000
}
FlaUI重要的元素类型
public enum ControlType
{
/// <summary>
/// Identifies an unknown control type.
/// </summary>
Unknown,
/// <summary>
/// Identifies the AppBar control type. Supported starting with Windows 8.1.
/// </summary>
AppBar,
/// <summary>
/// Identifies the Button control type.
/// </summary>
Button,
/// <summary>
/// Identifies the Calendar control type.
/// </summary>
Calendar,
/// <summary>
/// Identifies the CheckBox control type.
/// </summary>
CheckBox,
/// <summary>
/// Identifies the ComboBox control type.
/// </summary>
ComboBox,
/// <summary>
/// Identifies the Custom control type.
/// </summary>
Custom,
/// <summary>
/// Identifies the DataGrid control type.
/// </summary>
DataGrid,
/// <summary>
/// Identifies the DataItem control type.
/// </summary>
DataItem,
/// <summary>
/// Identifies the Document control type.
/// </summary>
Document,
/// <summary>
/// Identifies the Edit control type.
/// </summary>
Edit,
/// <summary>
/// Identifies the Group control type.
/// </summary>
Group,
/// <summary>
/// Identifies the Header control type.
/// </summary>
Header,
/// <summary>
/// Identifies the HeaderItem control type.
/// </summary>
HeaderItem,
/// <summary>
/// Identifies the Hyperlink control type.
/// </summary>
Hyperlink,
/// <summary>
/// Identifies the Image control type.
/// </summary>
Image,
/// <summary>
/// Identifies the List control type.
/// </summary>
List,
/// <summary>
/// Identifies the ListItem control type.
/// </summary>
ListItem,
/// <summary>
/// Identifies the MenuBar control type.
/// </summary>
MenuBar,
/// <summary>
/// Identifies the Menu control type.
/// </summary>
Menu,
/// <summary>
/// Identifies the MenuItem control type.
/// </summary>
MenuItem,
/// <summary>
/// Identifies the Pane control type.
/// </summary>
Pane,
/// <summary>
/// Identifies the ProgressBar control type.
/// </summary>
ProgressBar,
/// <summary>
/// Identifies the RadioButton control type.
/// </summary>
RadioButton,
/// <summary>
/// Identifies the ScrollBar control type.
/// </summary>
ScrollBar,
/// <summary>
/// Identifies the SemanticZoom control type. Supported starting with Windows 8.
/// </summary>
SemanticZoom,
/// <summary>
/// Identifies the Separator control type.
/// </summary>
Separator,
/// <summary>
/// Identifies the Slider control type.
/// </summary>
Slider,
/// <summary>
/// Identifies the Spinner control type.
/// </summary>
Spinner,
/// <summary>
/// Identifies the SplitButton control type.
/// </summary>
SplitButton,
/// <summary>
/// Identifies the StatusBar control type.
/// </summary>
StatusBar,
/// <summary>
/// Identifies the Tab control type.
/// </summary>
Tab,
/// <summary>
/// Identifies the TabItem control type.
/// </summary>
TabItem,
/// <summary>
/// Identifies the Table control type.
/// </summary>
Table,
/// <summary>
/// Identifies the Text control type.
/// </summary>
Text,
/// <summary>
/// Identifies the Thumb control type.
/// </summary>
Thumb,
/// <summary>
/// Identifies the TitleBar control type.
/// </summary>
TitleBar,
/// <summary>
/// Identifies the ToolBar control type.
/// </summary>
ToolBar,
/// <summary>
/// Identifies the ToolTip control type.
/// </summary>
ToolTip,
/// <summary>
/// Identifies the Tree control type.
/// </summary>
Tree,
/// <summary>
/// Identifies the TreeItem control type.
/// </summary>
TreeItem,
/// <summary>
/// Identifies the Window control type.
/// </summary>
Window
}
控件的使用
查找的AutomationElement通过函数 As类型函数转为相应的控件如:AutomationElement.AsListBoxItem();
ListBox
public class ListBox : AutomationElement
里面有不少方法含属性
如:
public ListBoxItem[] Items
{
。。。
}
public ListBoxItem Select(int index)
public ListBoxItem Select(string text)
public ListBoxItem RemoveFromSelection(int index)
public ListBoxItem RemoveFromSelection(string text)
ListBoxItem
public class ListBoxItem : SelectionItemAutomationElement
里面有不少方法含属性
如:
public virtual string Text{
...
}
FlaUI 控制ListBox滚动
int i=1;
var scrollPattern = listBox.Patterns.Scroll.Pattern;
var scroll = scrollPattern.VerticalViewSize * i;
if (scroll >1)
{
//如果滚动比例达到100%设置为1
scroll = 0;
}
//使用flaui组件将滚动面板的视图设置到滚动的位置
scrollPattern.SetScrollPercent(1, scroll);
i++;
await Task.Delay(100);
善于使用FlaUInspect查看元素属性
实用代码
//我们借助WINDOWS的两个API函数 ,先定义好API的C#调用方式。
//根据名称获取窗体句柄
[DllImport("user32.dll", EntryPoint = "FindWindow")]
private extern static IntPtr FindWindow(string lpClassName, string lpWindowName);
//根据句柄获取进程ID
[DllImport("User32.dll", CharSet = CharSet.Auto)]
public static extern int GetWindowThreadProcessId(IntPtr hwnd, out int ID);
int weChatID = 0;
IntPtr hwnd = FindWindow(null, "微信");
if (hwnd != IntPtr.Zero)
{
GetWindowThreadProcessId(hwnd, out weChatID);
}
//根据微信进程ID绑定FLAUI
var application = FlaUI.Core.Application.Attach(weChatID);
var automation = new UIA3Automation();
//获取微信window自动化操作对象
var Window = application.GetMainWindow(automation);
semantic-kernelGPT大模型编排工具
Semantic Kernel是一个轻量级的SDK,最基本的功能就是帮我们完成与OpenAI、Azure OpenAI和Hugging Face大模型的API的对接,并且支持C#、Python、Java版本。
Semantic Kernel提供自定义插件、编排计划、信息存储至数据库(如SQLite、MongoDB、Redis、Postgres等)。
总的来说,Semantic Kernel就是可以把用户输入的prompt,经过分解为多个步骤、获取外部数据、执行自定义操作等,转换为一个更好的prompt,再调用大模型API,从而获取结果。
以下是C#使用示例:
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
var builder = Kernel.CreateBuilder();
//OpenAI模型
builder.AddOpenAIChatCompletion(
"gpt-3.5-turbo",
"...your OpenAI API Key...");
var kernel = builder.Build();
//等待用户输入
Console.Write("用户:");
var input = Console.ReadLine();
//循环对话
while (input != "quit")
{
var prompt = @$"<message role=""user"">{input}</message>";
var summarize = kernel.CreateFunctionFromPrompt(prompt, executionSettings: new OpenAIPromptExecutionSettings { MaxTokens = 100 });
var result = kernel.InvokeStreamingAsync(summarize);
Console.Write("OpenAI:");
await foreach (var item in result)
{
Console.Write(item.ToString());
}
Console.WriteLine();
Console.WriteLine();
Console.Write("用户:");
input = Console.ReadLine();
}
ChatGPT桌面客户端
这是基于C#开发的客户端,兼容MacOS、Windows、Linux、Android、iOS系统,支持OpenAI_API_key自定义设置,还有API参数自定义设置。
PC端版本支持快捷键操作,内置了多种对换模式:助理、翻译、开发人员、技术文章作者。
该项目界面美观大气,不管是用于二次开发,还是学习用途,都是非常不错的选择。
1、跨平台:采用.Net 7.0、手机端采用:Xamarin,支持MacOS、Windows、Linux、Android、iOS。
2、UI框架: Avalonia UI。
Stable Diffusion客户端
StableSwarmUI是由官方推出的WebUI框架,真正的文生图和图生图的推理任务,还是在后端执行,官方推荐我们配合ComfyUI来使用。
StableSwarmUI专注让Stable Diffusion变得易于访问,核心特点是易用性、高性能和可扩展性,提升用户体验。
StableSwarmUI不仅支持多种语言,还引入图像编辑器、自动模型格式检测以及额外的生成类型(如视频)等功能。它在定制方面还是不错的,提供了预设、主题和服务器配置等控制权,无需用户手动调优,使得经验较少的用户也能轻松上手。
ChatGPT SDK
这个是根据OpenAI的开放API,封装的.Net SDK。目前官方的API都已经封装,包含生成文本、图片、获取模型等。
音频转文字
对话聊天模式
画图
StabilityMatrix为Stable Diffusion提供易于使用的软件包管理
Stability Matrix是基于.Net开发的开源项目,核心功能是为Stable Diffusion提供易于使用的软件包管理器。
它支持Stable Diffusion的Web UI软件包的一键安装和更新,并且提供了简单易用的用户界面,同时内嵌Git、Python等依赖,大大简化安装与配置的过程。
Hangfire是一个开源的.NET任务调度框架
Hangfire是一个开源的.NET任务调度框架,目前1.6+版本已支持.NET Core,可以用在ASP.NET应用执行多种类型的后台任务,无需额外开发后台服务。
同时Hangfire内置了集成化控制台,在上面可以清晰看到作业调度的情况,并且支持持久化的存储,支持有 Redis、SQL Server、SQL Azure 和 MSMQ。
任务场景示例
1、执行后台任务,执行一次
var jobId = BackgroundJob .Enqueue(
() => Console.WriteLine( "一劳永逸!" ));
2、延迟任务:7天后执行
var jobId = BackgroundJob .Schedule(
() => Console.WriteLine( "延迟!" ),
TimeSpan.FromDays(7));
3、重复任务:每天执行一次
RecurringJob .AddOrUpdate(
"myrecurringjob" ,
() => Console.WriteLine( "重复!" ),
Cron.Daily);
4、延续任务:在某个任务后执行
BackgroundJob .ContinueJobWith(
jobId,
() => Console .WriteLine( "继续!" ));
5、批量执行任务
var batchId = BatchJob.StartNew(x =>
{
x.Enqueue(() => Console.WriteLine("Job 1"));
x.Enqueue(() => Console.WriteLine("Job 2"));
});
6、批量延续任务
BatchJob.ContinueBatchWith(batchId, x =>
{
x.Enqueue(() => Console.WriteLine("Last Job"));
});
SkiaSharp:.NET强大而灵活的跨平台图形库
SkiaSharp是基于Google的Skia图形库的.NET封装,是一个用于2D图像绘制的开源库,无论桌面应用程序、移动应用还是Web应用,都可以使用。
.Net开发人员可以利用这个强大而灵活的跨平台图形库,来实现高质量的图形绘制和渲染。
1、跨平台:支持多种操作系统,包括Windows、macOS、iOS、Android以及其他.NET Core兼容的平台。
2、高性能:支持硬件加速技术,提供高效的图形渲染能力,都可以保证在任何平台应用流畅的用户体验;
3、易用性:提供了直观的API,使得开发人员可以轻松地在.NET应用程序中绘制各种形状、文本和图像。此外还提供了WPF和WinForms的控件,简化开发难度与工作量。
4、丰富的图形功能:除了提供基本的绘图操作,还提供很多复杂的图形效果,如阴影、渐变和纹理。
5、支持多种文本渲染:无论是矢量字体还是位图字体,都提供了强大的文本渲染能力。
验证码示例代码
using SkiaSharp;
//图片宽度
var width = 90;
//图片高度
var height = 30;
//生成随机验证码
var code = CreateValidateCode(4);
// 创建一个SkiaSharp画布
using (var surface = SKSurface.Create(new SKImageInfo(width, height)))
{
var canvas = surface.Canvas;
// 清除画布
canvas.Clear(SKColors.White);
// 使用SkiaSharp绘制验证码文本
using (var textPaint = new SKPaint())
{
textPaint.Color = SKColors.Black;
textPaint.IsAntialias = true;
textPaint.TextSize = height * 0.8f; // 设置文本大小
textPaint.StrokeWidth = 3;
var textBounds = new SKRect();
textPaint.MeasureText(code, ref textBounds);
var xText = (width - textBounds.Width) / 2;
var yText = (height - textBounds.Height) / 2 - textBounds.Top;
canvas.DrawText(code, xText, yText, textPaint);
}
// 绘制干扰线
using (var linePaint = new SKPaint())
{
// 半透明黑色
linePaint.Color = new SKColor(0, 0, 0, 128);
linePaint.StrokeWidth = 1;
linePaint.IsAntialias = true;
var random = new Random();
for (int i = 0; i < 5; i++) // 绘制5条干扰线
{
float x1 = 0;
float y1 = random.Next(height);
float x2 = width;
float y2 = random.Next(height);
canvas.DrawLine(x1, y1, x2, y2, linePaint);
}
}
// 保存图像到文件
using (var image = surface.Snapshot())
using (var data = image.Encode())
{
File.WriteAllBytes("code.png", data.ToArray());
}
}
/// <summary>
/// 随机生成验证码
/// </summary>
/// <param name="len"></param>
/// <returns></returns>
string CreateValidateCode(int len)
{
// 可选字符集
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
// 创建一个新的随机数生成器
Random random = new Random();
// 生成验证码
string code = new string(Enumerable.Repeat(chars, len)
.Select(s => s[random.Next(s.Length)]).ToArray());
return code;
}
用C#开发Excel插件的强大开源工具
Excel-DNA是一个.Net开源项目,为开发者提供了一种便利的方法,可以将.Net代码与Excel集成,能够轻松的为Excel创建自定义函数、图表、表单等,一方面不仅可以利用.Net强大的库,另外一方面还可以与外部数据、程序等连接交互。
使用示例
1、创建一个类型:类库的项目,这边选择.Net 6。
2、修改项目文件ClassLibrary.csproj
修改TargetFramework修改为net6.0-windows,示例代码如下:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
3、添加依赖库:Excel-DNA。
4、创建类并添加如下代码:
using ExcelDna.Integration;
public static class MyFunctions
{
[ExcelFunction(Description = ".Net自定义方法")]
public static string SayHello(string name)
{
return "Hello " + name;
}
[ExcelFunction(Description = ".Net自定义方法")]
public static int AddExt(int num1, int num2)
{
return num1 + num2;
}
}
5、运行项目,并在对话框选择:仅为本对话启用此加载项。
6、使用效果
SayHello自定义方法:
AddExt自定义方法:
功能强大、操作易用的屏幕录制.Net开源工具
该工具不仅支持全屏录制,还提供区域录制、游戏录制和摄像头录制等多种模式。不管是录制软件操作、游戏、直播、网络教学、课件制作还是在线视频,都可以满足你的需求。
此外该工具还可以录制多种屏幕内容,如鼠标点击和键盘的输入等。
强大PDF处理能力的.Net开源项目
itext7-dotnet是一个功能强大的库,专门为.Net设计,用于创建、编辑和操作PDF文件,可以帮我们快速、高效得处理PDF文件。
该项目支持创建各种类型的PDF文件,包含基本文本文档、表格、图像、连接等。还具有强大的编辑功能,比如调整页面布局、添加水印等。
此外还支持加密功能、国际化等特性、支持PDF/A、PDF/UA等。
使用示例
1、安装依赖库
2、示例代码
using iText.Kernel.Pdf;
using iText.Layout;
using iText.Layout.Element;
using iText.Layout.Properties;
// 创建一个新的PDF文档
PdfDocument pdf = new PdfDocument(new PdfWriter("output.pdf"));
Document document = new Document(pdf);
// 添加一个表格到PDF中
Table table = new Table(3); // 3列的表格
table.SetWidth(100); // 设置表格宽度为100%
table.SetHorizontalAlignment(HorizontalAlignment.CENTER); // 居中对齐
table.AddCell("Name"); // 添加表头
table.AddCell("Age");
table.AddCell("Country");
table.AddCell("John Doe"); // 添加行数据
table.AddCell("30");
table.AddCell("USA");
table.AddCell("Jane Smith");
table.AddCell("25");
table.AddCell("UK");
table.AddCell("Bob Johnson");
table.AddCell("40");
table.AddCell("Canada");
document.Add(table);
// 关闭文档
document.Close();
一个Star 4.1K的.Net开源CMS项目
Umbraco CMS开发者团队是来源于丹麦,经过多年的发展,已经成为全球比较知名并被广泛使用的CMS,它以友好的用户体验和高度可定制性而出名,非常适合用于开发各种类型网站项目,比如开发企业网站、电子商务系统、企业管理系统等。
推荐一个Star 1.3K报表.Net开源项目
Seal-Report是一个基于.NET框架的开源项目,提供了简单、直观的报表和报告功能,具有报表设计器,可减少复杂的配置,无需编程知识也可以使用。采用该项目,就可以为企业快速提供高质量的报表,从而提升工作效率和决策速度
一个让你轻松搭建漫画、小说网站的.Net开源项目
这是一个基于.Net开发的开源项目,该项目的核心功能是可以上传漫画至服务器,并可以在浏览器查看和管理漫画。
该项目支持灵活的阅读分组和管理、强大的用户管理功能、强大的网页阅读器功能、本地化支持、可定制的仪表板和侧边栏。
该项目功能完整,方便开发人员快速搭建一个漫画网站,或者进行二次开发。
该项目基于ASP.NET Core框架构建,支持通过Docker容器来运行。
ZXing.Net二维码开发库
using ZXing;
var codeReader = new BarcodeReader();
// 读取图像文件
var bitmap = (Bitmap)Image.FromFile("e:\\1.png");
var result = codeReader.Decode(bitmap);
if (result != null)
{
MessageBox.Show(result.Text);
}
else
{
MessageBox.Show("No QR code found.");
}
当使用ZXing.NET库识别一张图片上的多个二维码时,可以按照以下示例代码来实现
using System;
using System.Collections.Generic;
using System.Drawing;
using ZXing;
public class MultiQRCodeReader
{
public static void Main(string[] args)
{
// 加载包含二维码的图像
Bitmap image = new Bitmap("your_image_path.jpg");
// 创建一个BarcodeReader对象并设置解码参数
BarcodeReader reader = new BarcodeReader();
reader.Options = new DecodingOptions
{
PossibleFormats = new List<BarcodeFormat> { BarcodeFormat.QR_CODE },
TryHarder = true
};
// 调用DecodeMultiple方法识别图像中的多个二维码
Result[] results = reader.DecodeMultiple(image);
// 处理识别结果
if (results != null && results.Length > 0)
{
Console.WriteLine($"Found {results.Length} QR codes:");
foreach (Result result in results)
{
Console.WriteLine($"QR Code Text: {result.Text}");
Console.WriteLine($"QR Code Format: {result.BarcodeFormat}");
Console.WriteLine();
}
}
else
{
Console.WriteLine("No QR codes found in the image.");
}
}
}
PaddleOCRSarp是一个基于百度飞桨PaddleOCR的C++代码封装
PaddleOCRSharp 是一个基于 PaddlePaddle 深度学习框架的 OCR(光学字符识别)库的 C# 封装。它提供了一种方便的方式来进行文字检测、文本识别和版面分析等任务。下面是 PaddleOCRSharp 的使用方法的详细介绍:
一、安装 PaddleOCRSharp
PaddleOCRSharp 可以通过 NuGet 包管理器进行安装。在 Visual Studio 中打开项目,右键单击项目,并选择 "管理 NuGet 包"。在搜索栏中搜索 "PaddleOCRSharp",然后点击 "安装" 安装包。
二、导入必要的命名空间
在代码文件的顶部,添加以下命名空间引用:
using PaddleOCRSharp;
using PaddleOCRSharp.Config;
using PaddleOCRSharp.Utils;
三、创建 OCR 实例
使用以下代码创建一个 OCR 实例:
var ocr = new PaddleOCR();
四、配置 OCR 模型
PaddleOCRSharp 提供了多种模型供选择。你可以使用预训练好的模型,也可以加载自定义的模型。以下是一个示例,展示如何使用英文识别模型:
var config = new EnglishOCRConfig();
ocr.SetConfig(config);
五、运行 OCR
将图像传递给 OCR 实例进行处理,并获取检测到的文字结果:
var image = ImageUtil.LoadImage("path/to/image.jpg");
var result = ocr.Run(image);
六、处理 OCR 结果
OCR 结果将作为一个列表返回,列表中每个元素都是一个字典,包含识别到的文字和其对应的位置信息。你可以使用以下代码处理结果:
foreach (var item in result)
{
var text = item["text"];
var confidence = item["confidence"];
var location = item["location"]; // 文字所在的矩形位置信息
// Do something with the text, confidence and location...
}
以上就是使用 PaddleOCRSharp 的基本步骤。你可以根据自己的需求选择合适的模型,并进行相应的配置和处理。更多详细的使用方法和示例代码可以参考 PaddleOCRSharp 的官方文档。
七、其他
OCRModelConfig config = null;
OCRParameter oCRParameter = new OCRParameter();
oCRParameter.numThread = 6; // 预测并发线程数
oCRParameter.Enable_mkldnn = 1; // web部署该值建议设置为0,否则出错,内存如果使用很大,建议该值也设置为0.
oCRParameter.cls = 1; // 是否执行文字方向分类;默认false
oCRParameter.use_angle_cls = 1; // 是否开启方向检测,用于检测识别180旋转
oCRParameter.det_db_score_mode = 1; // 是否使用多段线,即文字区域是用多段线还是用矩形
oCRParameter.UnClipRatio = 1.6F;
oCRParameter.MaxSideLen = 2000;
// 初始化OCR引擎
PaddleOCREngine engine = new PaddleOCREngine(config, oCRParameter);
请注意,由于代码片段中的 OCRModelConfig 类的定义不在提供的范围内,因此我将其定义为一个变量,并将其设置为 null。你需要根据实际情况将其替换为正确的类型和创建逻辑。
另外,请确保在代码文件的顶部添加相应的命名空间引用:
using PaddleOCRSharp.Config;
using PaddleOCRSharp.Utils;
PaddleSharp支持14种OCR语言模型的按需下载,允许旋转文本角度检测,180度文本检测,同时也支持表格识别
Install NuGet Packages:
Sdcb.PaddleInference
Sdcb.PaddleOCR
Sdcb.PaddleOCR.Models.Local
Sdcb.PaddleInference.runtime.win64.mkl
OpenCvSharp4.runtime.win
(1)、添加项目“Sdcb.PaddleOCR”的引用
(2)、添加项目“Sdcb.PaddleOCR.KnownModels”的引用
(3)、添加项目“Sdcb.PaddleInference”的引用
(4)、nuget添加“Sdcb.PaddleInference.runtime.win64.mkl”
(5)、nuget添加“OpenCvSharp4”
(6)、nuget添加“OpenCvSharp4.runtime.win”
代码
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using OpenCvSharp;
using Sdcb.PaddleOCR;
using Sdcb.PaddleOCR.KnownModels;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
namespace OcrServerAPI.Controllers
{
[ApiController]
[Route("[controller]")]
public class OcrController : ControllerBase
{
private readonly ILogger<OcrController> _logger;
public OcrController(ILogger<OcrController> logger)
{
_logger = logger;
}
[HttpGet]
public string Get()
{
var ocrResult = DoOcr().Result;
return ocrResult;
}
private async Task<string> DoOcr()
{
var strResult = string.Empty;
OCRModel model = KnownOCRModel.PPOcrV2;
await model.EnsureAll();
byte[] sampleImageData;
string sampleImageUrl = @"https://www.tp-link.com.cn/content/images/detail/2164/TL-XDR5450易展Turbo版-3840px_03.jpg";
using (HttpClient http = new HttpClient())
{
Console.WriteLine("Download sample image from: " + sampleImageUrl);
sampleImageData = await http.GetByteArrayAsync(sampleImageUrl);
}
using (PaddleOcrAll all = new PaddleOcrAll(model.RootDirectory, model.KeyPath)
{
AllowRotateDetection = true, /* 允许识别有角度的文字 */
Enable180Classification = false, /* 允许识别旋转角度大于90度的文字 */
})
{
// Load local file by following code:
// using (Mat src2 = Cv2.ImRead(@"C:\test.jpg"))
using (Mat src = Cv2.ImDecode(sampleImageData, ImreadModes.Color))
{
PaddleOcrResult result = all.Run(src);
Console.WriteLine("Detected all texts: \n" + result.Text);
strResult = result.Text;
foreach (PaddleOcrResultRegion region in result.Regions)
{
Console.WriteLine($"Text: {region.Text}, Score: {region.Score}, RectCenter: {region.Rect.Center}, RectSize: {region.Rect.Size}, Angle: {region.Rect.Angle}");
}
}
}
return strResult;
}
}
}
可能会报错
解决:
找到原因:根据上面提示的路径:C:\Users\wjx\AppData\Roaming\paddleocr-models\ppocr-v2\key.txt文件缺失。
下载文件:文件路径如下:https://gitee.com/paddlepaddle/PaddleOCR/blob/release/2.4/ppocr/utils/ppocr_keys_v1.txt
复制文件:把上一步下载下来的文件改名为:key.txt,然后放到C:\Users\wjx\AppData\Roaming\paddleocr-models\ppocr-v2 目录下,如下图:
protobuf-net介绍
一、什么是Protobuf
Protobuf 是 Google 公司提供的一款简洁高效且开源的二进制序列化数据存储方案。只要遵循其PB语法定义的消息格式,然后通过批处理,就可以生成目标代码。
二、为什么要使用Protobuf
三、Protobuf的序列化和反序列化
序列化 : 将 数据结构或对象 转换成 二进制的过程, 就是序列化。
反序列化 : 将 二进制 转换成 数据结构或对象的过程, 就是反序列化。
三、Protobuf语法
下面详细介绍.proto的消息对象&字段:
1、消息对象
该对象,可以理解为class/struct结构。
在一个.proto文件中可以嵌套多个消息对象。
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
// 该消息类型 定义在 Person消息类型的内部
// 即Person消息类型 是 PhoneNumber消息类型的父消息类型
message PhoneNumber {
required string number = 1;
}
}
<-- 多重嵌套 -->
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
required int64 ival = 1;
optional bool booly = 2;
}
}
}
注意:尽可能的将某一消息类型对应的响应消息格式都定义在一个proto文件中。如:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
// 与SearchRequest消息类型 对应的 响应消息类型SearchResponse
message SearchResponse {
…
}
2、字段
消息对象的字段 组成主要是:字段 = 字段修饰符 + 字段类型 +字段名 +标识号
下面对字段修饰符,进行详细描述。字段修饰符是字段解析时的规则:
字段类型,主要有三个:
基本数据 类型
枚举 类型
消息对象 类型
复制代码
1 message Person {
2
3 // 基本数据类型 字段
4 required string name = 1;
5 required int32 id = 2;
6 optional string email = 3;
7
8 enum PhoneType {
9 MOBILE = 0;
10 HOME = 1;
11 WORK = 2;
12 }
13
14 message PhoneNumber {
15 optional PhoneType type = 2 [default = HOME];
16 // 枚举类型 字段
17 }
18
19 repeated PhoneNumber phone = 4;
20 // 消息类型 字段
21 }
复制代码
(1)基本的数据类型,如下:
2)枚举类型
作用:为字段指定一个 可能取值的字段集合 (该字段只能从 该指定的字段集合里 取值)
说明:如下面例子,电话号码 可能是手机号、家庭电话号或工作电话号的其中一个,那么就将PhoneType定义为枚举类型,并将加入电话的集合( MOBILE、 HOME、WORK)
// 枚举类型需要先定义才能进行使用
// 枚举类型 定义
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
// 电话类型字段 只能从 这个集合里 取值
}
// 特别注意:
// 1. 枚举类型的定义可在一个消息对象的内部或外部
// 2. 都可以在 同一.proto文件 中的任何消息对象里使用
// 3. 当枚举类型是在一消息内部定义,希望在 另一个消息中 使用时,需要采用MessageType.EnumType的语法格式
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
// 使用枚举类型的字段(设置了默认值)
}
// 特别注意:
// 1. 枚举常量必须在32位整型值的范围内
// 2. 不推荐在enum中使用负数:因为enum值是使用可变编码方式的,对负数不够高
(3)消息对象 类型
一个消息对象 可以将 其他消息对象类型 用作字段类型,情况如下:
3.1使用同一个 .proto 文件里的消息类型
a. 使用 内部消息类型
目的:先在 消息类型 中定义 其他消息类型 ,然后再使用
即嵌套,需要 用作字段类型的 消息类型 定义在 该消息类型里
实例:
复制代码
1 message Person {
2 required string name = 1;
3 required int32 id = 2;
4 optional string email = 3;
5
6 // 该消息类型 定义在 Person消息类型的内部
7 // 即Person消息类型 是 PhoneNumber消息类型的父消息类型
8 message PhoneNumber {
9 required string number = 1;
10 }
11
12 repeated PhoneNumber phone = 4;
13 // 直接使用内部消息类型
14 }
复制代码
b、使用 外部消息类型
即外部重用,需要 用作字段类型的消息类型 定义在 该消息类型外部
复制代码
1 message Person {
2 required string name = 1;
3 required int32 id = 2;
4 optional string email = 3;
5 }
6
7 message AddressBook {
8 repeated Person person = 1;
9 // 直接使用了 Person消息类型作为消息字段
10 }
复制代码
c. 使用 外部消息的内部消息类型
复制代码
1 message Person {
2 required string name = 1;
3 required int32 id = 2;
4 optional string email = 3;
5
6 // PhoneNumber消息类型 是 Person消息类型的内部消息类型
7 message PhoneNumber {
8 required string number = 1;
9 optional PhoneType type = 2 [default = HOME];
10 }
11 }
12
13 // 若父消息类型外部的消息类型需要重用该内部消息类型
14 // 需要以 Parent.Type 的形式去使用
15 // Parent = 需要使用消息类型的父消息类型,Type = 需要使用的消息类型
16
17 // PhoneNumber父消息类型Person 的外部 OtherMessage消息类型 需要使用 PhoneNumber消息类型
18 message OtherMessage {
19 optional Person.PhoneNumber phonenumber = 1;
20 // 以 Parent.Type = Person.PhoneNumber 的形式去使用
21
22 }
复制代码
3.2 使用不同 .proto 文件里的消息类型
目的:需要在 A.proto文件 使用 B.proto文件里的消息类型
解决方案:在 A.proto文件 通过导入( import) B.proto文件中来使用 B.proto文件 里的消息类型
1 import "myproject/other_protos.proto"
2 // 在A.proto 文件中添加 B.proto文件路径的导入声明
3 // ProtocolBuffer编译器 会在 该目录中 查找需要被导入的 .proto文件
4 // 如果不提供参数,编译器就在 其调用的目录下 查找
当然,在使用 不同 .proto 文件里的消息类型 时 也会存在想 使用同一个 .proto 文件消息类型的情况,但使用都是一样,此处不作过多描述。
C#使用
Install-Package protobuf-net
Basic usage
1 First Decorate your classes
[ProtoContract]
class Person {
[ProtoMember(1)]
public int Id {get;set;}
[ProtoMember(2)]
public string Name {get;set;}
[ProtoMember(3)]
public Address Address {get;set;}
}
[ProtoContract]
class Address {
[ProtoMember(1)]
public string Line1 {get;set;}
[ProtoMember(2)]
public string Line2 {get;set;}
}
Note that unlike XmlSerializer, the member-names are not encoded in the data - instead, you must pick an integer to identify each member. Additionally, to show intent it is necessary to show that we intend this type to be serialized (i.e. that it is a data contract).
2 Serialize your data
This writes a 32 byte file to "person.bin" :
var person = new Person {
Id = 12345, Name = "Fred",
Address = new Address {
Line1 = "Flat 1",
Line2 = "The Meadows"
}
};
using (var file = File.Create("person.bin")) {
Serializer.Serialize(file, person);
}
3 Deserialize your data
This reads the data back from "person.bin" :
Person newPerson;
using (var file = File.OpenRead("person.bin")) {
newPerson = Serializer.Deserialize<Person>(file);
}
知乎对于protobuf-net的讲解
Protobuf-net是一套开源的第三方库,提供了将.proto文件转换成协议类的工具,并且实现了对协议对象进行编码解码的方法。它有两种使用方式,第一种和protobuf使用流程基本一样,需要编写好.proto文件再进行编译;第二种则只要为字段添加特性,使用起来非常方便,这也是我们使用它的主要原因
通过.proto文件方式使用
首先在GitHub上下载,地址如下:protobuf-net
解压之后主要有三个文件夹,分别是
assorted:这是重构系统时遗留下的部分内容,目前我们可以不管它
docs:使用文档
src:主要源文件
在src文件夹下,最需要关注下面三个文件夹:
protobuf-net:核心工程,用于序列化与反序列化等操作。
protogen:用于将标准的protobuf文件 *.proto 转换成 *.cs 文件。
首先对项目进行编译,使用visual studio打开src中的protobuf-net.sln,点击生解决方案(此时可能会有一些错误提示,但只要上面说的三个重点文件能成功生成,就问题不大)。
打开protogen所在的文件夹,在其bin目录下找到protogen.exe文件,我们要通过它将 *.proto 转换成 *.cs 文件。
创建 .proto 文件
首先我们要创建一个 Person.proto 文件,指定其版本和命名空间,并定义具体的消息(message),message就是一组字段的集合,在C#中可以理解为一个类,详细信息可以参考前一篇文章
syntax = "proto3";
//指定命名空间(可选参数)如果没有,则使用C#代码的命名空间。
option csharp_namespace = "ProtoTest"
message Person
{
string name = 1;
int32 id = 2;
enum Sex
{
male = 0;
female = 1;
}
Sex sex = 3;
repeated string friends = 4;
}
“repeated”表示重复字段,在C#中被编译成List。
编译成 .cs 文件
通过protogen,可以很方便的将 .proto 编译成 .cs 文件,找到之前编译的protogen.exe程序,打开windows powershell或者cmd,切换到protogen.exe根目录,我使用的是powershell。
使用命令行 .\protogen --version 查看它的版本(cmd中不需要加 " .\ " ),使用 .\protogen -h查看帮助。
确定protogen有效之后,便可以着手编译代码了。为了方便演示,我们新创建一个文件夹proto,将Person.proto放进去
接下来执行命令
.\protogen Person.proto --csharp_out=""
可以看到,proto文件夹中已经包含Person.cs了。
生成代码的指令与文档描述指令有点不一样,当时按照文档尝试了好久一直无法生成cs文件,查看代码发现,它会执行Path.Combine函数将路径合并。
/// <summary>
/// 保存cs文件
/// </summary>
/// <param name="files">proto文件</param>
/// <param name="outPath">保存文件的路径</param>
internal static void WriteFiles(IEnumerable<CodeFile> files, string outPath)
{
foreach (var file in files)
{
var path = Path.Combine(outPath, file.Name);
var dir = Path.GetDirectoryName(path);
if (!Directory.Exists(dir))
{
Console.Error.WriteLine($"Output directory does not exist, creating... {dir}");
Directory.CreateDirectory(dir);
}
File.WriteAllText(path, file.Text);
}
}
protogen 2.0 和 3.0 版本也有所不同,具体以文档为准
查看和使用编译后的文件
打开生成的Person类,发现它继承自global::ProtoBuf.IExtensible,具体如下
namespace ProtoTest
{
[global::ProtoBuf.ProtoContract()]
public partial class Person : global::ProtoBuf.IExtensible
{
private global::ProtoBuf.IExtension __pbn__extensionData;
global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
=> global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
[global::ProtoBuf.ProtoMember(1, Name = @"name")]
[global::System.ComponentModel.DefaultValue("")]
public string Name { get; set; } = "";
[global::ProtoBuf.ProtoMember(2, Name = @"id")]
public int Id { get; set; }
[global::ProtoBuf.ProtoMember(3)]
public Sex sex { get; set; }
[global::ProtoBuf.ProtoMember(4, Name = @"friends")]
public global::System.Collections.Generic.List<string> Friends { get; } = new global::System.Collections.Generic.List<string>();
[global::ProtoBuf.ProtoContract()]
public enum Sex
{
[global::ProtoBuf.ProtoEnum(Name = @"male")]
Male = 0,
[global::ProtoBuf.ProtoEnum(Name = @"female")]
Female = 1,
}
}
}
对于[global::ProtoBuf.ProtoContract()]、 [global::ProtoBuf.ProtoMember()]这些特性(Attribute)可以稍微留意一下,后面会具体讲解。
对类进行编码
编码的时候主要是调用ProtoBuf.Serializer方法,它将协议对象转化成字节。
public static byte[] Encode(IExtensible msgBase)
{
using (MemoryStream memory = new MemoryStream())
{
Serializer.Serialize(memory, msgBase);
return memory.ToArray();
}
}
对类进行解码
解码用到的是Serializer.Deserialize方法,使用反射通过类名获取类
public static IExtensible Decode(string protoName, byte[] bytes, int offset, int count)
{
using (MemoryStream memory = new MemoryStream(bytes, offset, count))
{
Type t = Type.GetType(protoName);
return (IExtensible)Serializer.Deserialize(t, memory);
}
}
测试Person类
随便建立一个脚本,此处在本地进行测试发现可以读取数据内容
private void Start()
{
Person p = new Person
{
Id = 1,
Name = "p1",
sex = Person.Sex.Male
};
p.Friends.Add("taylor");
byte[] val = ProtoTools.Encode(p);
Person newP = ProtoTools.Decode("Tank.Person", val, 0, val.Length) as Person;
Debug.Log(newP.Id + newP.Name + newP.sex + p.Friends[0]); //成功打印出数据
}
通过添加特性的方式使用
安装 protobuf-net 包
在Visual Studio中,选中 工具 -> NuGet 包管理器 -> 程序包管理器控制台
然后输入指令
Install-Package protobuf-net
或者在NuGet解决方案中直接搜索 protobuf-net 进行安装
声明要进行序列化的类
序列化的类需要通过 [ProtoContract] 进行标识,里面的属性/字段则需要使用[ProtoMember(x)]标识,因为在序列化的时候,将使用整数x替换字段名称,这样能够节省大量内存
[ProtoContract]
class Person {
[ProtoMember(1)]
public int Id {get;set;}
[ProtoMember(2)]
public string Name {get;set;}
[ProtoMember(3)]
public Address Address {get;set;}
}
[ProtoContract]
class Address {
[ProtoMember(1)]
public string Line1 {get;set;}
[ProtoMember(2)]
public string Line2 {get;set;}
}
对于字段标识符,有以下几点需要注意:
它们必须是正整数(为了更好的可移植性,x应该 <= 536870911 并且不再 19000-19999 范围内)
在单个类中,它们必须是唯一的
标识符不能与任何继承标识符冲突
数字越小占用的内存也越小,因此不要从100000 这种很大的值开始
标识符十分重要,修改成员名称没有关系,但修改标识符就相当于修改了数据
对类进行序列化
将创建的类序列化并写入到 person.bin 文件中
var person = new Person {
Id = 12345, Name = "Fred",
Address = new Address {
Line1 = "Flat 1",
Line2 = "The Meadows"
}
};
using (var file = File.Create("person.bin")) {
Serializer.Serialize(file, person);
}
对文件反序列化
将刚才的bin文件反序列化成Person类
Person newPerson;
using (var file = File.OpenRead("person.bin")) {
newPerson = Serializer.Deserialize<Person>(file);
}
在Unity使用protobuf-net
将编译的文件复制到Unity
将Person.cs复制到unity工作目录,此时unity报了很多错误,这是因为缺少dll文件。接下来找到protobuf-net的生成位置
这里生成了很多个版本的dll,找到适合自己的版本,我使用的是net461,将里面的dll全部拷贝到unity的Plugins文件夹中
待Unity重新编译后,所有错误消失。
如果在Visual Studio中通过NuGet 安装,可以项目的Package目录下找到对应的dll文件,并将其放入Unity的Plugins文件夹中即可。
类的继承
继承的类必须使用 [ProtoInclude(...)] 关键字进行显式声明。
[ProtoContract]
[ProtoInclude(7, typeof(SomeDerivedType))]
class SomeBaseType {...}
[ProtoContract]
class SomeDerivedType {...}
代码中的数字7并没有特殊含义,就和[ProtoMemeber()]一样,但在SomeBaseType类中必须是唯一的。
序列化多个类
同时序列化多个类,可以使用SerializeWithLengthPrefix方法,该方法会自动记录序列化的位置,如下图所示
要读取的时候,先读取第一个消息的长度,再对其内容进行序列化,然后开始第二个,以此类推......很明显的是,它需要占用额外的内存来记录消息长度,消息长度默认占用4byte
MemoryStream stream = new MemoryStream();
CSPacketHeader head = new CSPacketHeader()
{
type = ProtoMsg.MsgType.CSMatching
};
CSHeartBeat heartBeat = new CSHeartBeat();
CSLogin login = new CSLogin()
{
Pwd = "123",
UserName = "hello"
};
//序列化
RuntimeTypeModel.Default.SerializeWithLengthPrefix(stream, head, head.GetType(), PrefixStyle.Fixed32, 0);
RuntimeTypeModel.Default.SerializeWithLengthPrefix(stream, login, login.GetType(), PrefixStyle.Fixed32, 0);
using (var s = new MemoryStream(stream.GetBuffer()))
{
//反序列化
CSPacketHeader headVal = Serializer.DeserializeWithLengthPrefix<CSPacketHeader>(s,PrefixStyle.Fixed32);
Debug.LogError(headVal.type);
CSHeartBeat heartBeatVal = Serializer.DeserializeWithLengthPrefix<CSHeartBeat>(s, PrefixStyle.Fixed32);
Debug.LogError(heartBeatVal.Id);
}
注:反序列化的顺序必须和序列化一致,否则可能得到错误的结果
在使用的时候,遇到了一个很奇怪的问题,调用函数Serializer.SerializeWithLengthPrefix的时候,一直提示Type is not expected, and no contract can be inferred:xxxClass,但这个类使用Serializer.Serialize都可以进行序列化,最终的解决方案是RuntimeTypeModel.Default.SerializeWithLengthPrefix(stream, packet, packet.GetType(), PrefixStyle.Fixed32, 0);,显式指定要序列化的类型
总结
PdfiumViewer或PdfSharp来实现流式加载PDF并显示
流式加载PDF并显示
Install-Package PdfiumViewer
using System;
using System.IO;
using System.Threading.Tasks;
using PdfiumViewer;
namespace StreamPdfDemo
{
class Program
{
static async Task Main(string[] args)
{
// 读取PDF文件的字节数组
byte[] pdfBytes = File.ReadAllBytes("E:\\book\\Go Web编程.pdf");
// 将字节数组转换为MemoryStream
using (MemoryStream pdfStream = new MemoryStream(pdfBytes))
{
// 使用PdfiumViewer加载PDF
using (var document = PdfDocument.Load(pdfStream))
{
int dpiX=96;
int dpiY=96;
// 获取第一页
var page = document.Render(0, (int)document.PageSizes[0].Width, (int)document.PageSizes[0].Height, dpiX, dpiY, false);
// 保存渲染后的图像到文件
using (FileStream outputStream = new FileStream("output.png", FileMode.Create))
{
page.Save(outputStream, System.Drawing.Imaging.ImageFormat.Png);
}
// 获取PDF文件的总页数
int pageCount = document.PageCount;
}
}
Console.WriteLine("PDF已成功渲染为图片。");
}
}
}
PDF显示控件编译后引入PdfiumViewer.dll入VS Studio
Python.NET执行python脚本
安装Python.NET需要以下步骤:
安装Python:
首先,你需要在你的系统上安装Python。Python.NET支持Python 2.7和Python 3.x。
你可以从Python的官方网站(https://www.python.org)下载适合你的操作系统的Python安装程序。
在安装过程中,确保选择了"Add Python to PATH"选项,这样你就可以从命令行中访问Python。
安装.NET开发环境:
Python.NET是一个.NET库,所以你需要有一个.NET的开发环境。
如果你使用的是Visual Studio,它已经包含了.NET开发环境。
如果你没有Visual Studio,你可以安装.NET SDK(https://dotnet.microsoft.com/download)。
安装Python.NET:
Python.NET可以通过NuGet包管理器安装。NuGet是.NET的标准包管理系统。
如果你使用Visual Studio:
在你的项目中,右击"References",选择"Manage NuGet Packages"。
搜索"Python.NET",选择适当的版本(通常是最新的稳定版),然后点击"Install"。
如果你使用.NET CLI:
在你的项目目录中打开一个命令行。
运行命令:dotnet add package Python.NET。
配置Python.NET:
在使用Python.NET之前,你需要配置它以找到正确的Python安装。
Python.NET会尝试自动检测你的Python安装,但如果它无法找到,你可能需要手动配置。
你可以在你的.NET项目的app.config或web.config文件中添加一个<PythonDLL>条目来指定Python的DLL路径。例如:
<configuration>
<appSettings>
<add key="PythonDLL" value="C:\Python37\python37.dll"/>
</appSettings>
</configuration>
使用Python.NET可以方便地在C#中执行Python脚本。下面是一个示例,演示如何在C#中使用Python.NET执行一个Python脚本文件:
using System;
using System.IO;
using Python.Runtime;
class Program
{
static void Main(string[] args)
{
// 初始化Python解释器
PythonEngine.Initialize();
// 获取Python的全局解释器锁(GIL)
using (Py.GIL())
{
// 创建一个Python范围
using (PyScope scope = Py.CreateScope())
{
// 读取Python脚本文件
string scriptPath = "path/to/your/script.py";
string scriptCode = File.ReadAllText(scriptPath);
// 执行Python脚本
scope.Exec(scriptCode);
// 获取Python脚本中的变量
PyObject result = scope.Get("result");
// 将Python对象转换为.NET对象
int resultValue = result.As<int>();
Console.WriteLine($"Result from Python script: {resultValue}");
}
}
// 关闭Python解释器
PythonEngine.Shutdown();
}
}
在这个例子中:
我们首先初始化Python解释器(PythonEngine.Initialize())。
然后,我们使用Py.GIL()获取Python的全局解释器锁(GIL)。这是必需的,因为Python解释器是单线程的。
我们创建一个Python范围(PyScope),它表示一个独立的Python执行环境。
我们读取包含Python代码的脚本文件。
我们使用scope.Exec()执行这个Python脚本。这会在Python解释器中执行脚本,就像在Python中执行一样。
如果Python脚本中定义了变量,我们可以使用scope.Get()获取这些变量的值。
我们可以使用PyObject.As<T>()将Python对象转换为.NET对象。
最后,我们关闭Python解释器(PythonEngine.Shutdown())。
需要注意的是,Python脚本中的输出(如print语句)不会自动显示在C#的控制台中。如果你需要捕获Python脚本的输出,你可以重定向Python的标准输出流。
另外,如果你的Python脚本依赖于某些第三方包,确保这些包在Python的模块搜索路径中可用。你可能需要设置PYTHONPATH环境变量来指定这些包的位置。
使用Python.NET执行Python脚本提供了一种灵活的方式来在.NET应用中集成Python的功能。它允许你利用Python丰富的库生态系统,同时保持.NET的强类型和性能优势。
FaceRecognitionDotNet。这是一个基于 dlib 的人脸识别库的.NET 封装
using System;
using FaceRecognitionDotNet;
namespace FaceRecognitionDemo
{
class Program
{
static void Main(string[] args)
{
// 创建 FaceRecognition 实例,假设你已经下载了预训练模型并解压到 models 目录下
var faceRecognition = FaceRecognition.Create("models");
// 加载一张图片
using (var image = FaceRecognition.LoadImageFile("your_image.jpg"))
{
// 检测图片中的所有人脸
var faceLocations = faceRecognition.FaceLocations(image);
foreach (var faceLocation in faceLocations)
{
Console.WriteLine($"Top: {faceLocation.Top}, Right: {faceLocation.Right}, Bottom: {faceLocation.Bottom}, Left: {faceLocation.Left}");
}
}
}
}
}
C# winform免费开源UI框架
ReaLTaiizor是一个基于.Net的开源WinForm UI库(****)
ReaLTaiizor 是一个用户友好且注重设计的 .NET WinForms 项目控件库,包含各种组件。您可以使用不同的主题选项个性化您的项目,并自定义用户控件以使您的应用程序更加专业。
该项目还给出非常多的示例,这些示例包含:原神、卡巴斯基、MP3播放器、
安装依赖库
Install-Package ReaLTaiizor
支持换肤的开源组件MaterialSkin(*****)
Install-Package MaterialSkin
C# (Form1.cs)
public partial class Form1 : MaterialForm
.NETFramework Krypton
与 Visual Studio 一起使用
启动Visual Studio并创建/打开 Windows 窗体项目
打开应用程序的主窗体并显示工具箱
右键单击工具箱并添加选项卡,将其命名为Krypton
在新选项卡内单击鼠标右键,然后选择“选择项目”
单击“浏览”并转到Bin目录,然后选择所有ComponentFactory.Krypton...程序集
选择“确定”,现在它们全部都在工具箱中了!
源目录包含您可以查看、修改和直接编译的完整源代码。Krypton Components子目录包含所有实际控件,其他目录包含无数示例项目。
49 个基本控件,具有完整且一致的主题。
SunnyUI库
个人学习交流免费,商业应用需要授权
N_m3u8DL-RE跨平台的DASH/HLS/MSS下载工具。支持点播、直播(DASH/HLS)。
ConfuserEx是一个功能强大的开源.NET程序集保护工具
ConfuserEx是一个开源的.NET程序集保护工具,基于MIT许可证发布,可以免费使用。你可以从它的官方GitHub仓库下载源代码和编译好的二进制文件:
https://github.com/yck1509/ConfuserEx
支持.NET Framework 2.0/3.0/3.5/4.0/4.5
该项目停止维护,新的分支
该项目以对其进行修改和更新。
https://github.com/XenocodeRCE/neo-ConfuserEx
作为一个开源项目,ConfuserEx由社区维护和更新。它提供了以下主要功能:
重命名混淆 - 对程序集、类型、成员等名称进行混淆,增加逆向工程难度。
控制流混淆 - 对方法的控制流进行混淆,插入无用指令,使得反编译出的代码难以阅读。
常量加密 - 对常量字符串、数值等进行加密,防止静态分析提取敏感信息。
引用代理 - 对成员引用进行混淆,隐藏代码之间的调用关系。
防御tampering - 对程序集进行数字签名校验,防止被篡改。
防止调试 - 插入一些反调试逻辑,阻止动态调试分析。
资源压缩加密 - 对程序集的资源进行压缩和加密,减小体积并保护资源文件。
尽管ConfuserEx是免费的,但它的功能对于一般的.NET程序保护来说已经足够强大。当然,它可能无法与一些商业.NET保护工具相比,在性能、稳定性、服务支持等方面有一定差距。
作为开源软件,你可以根据自己的需求修改ConfuserEx的源代码,定制保护规则。但这需要你对.NET运行时和逆向工程技术有一定的了解。
总的来说,如果预算有限,或者是学习.NET保护技术,ConfuserEx是一个不错的选择。对于商业软件,尤其是安全要求较高的,建议还是选择专业的商业保护工具,并制定完善的软件保护方案。
开源跨平台的.NET保护工具ConfuserEx为例,演示如何对一个简单的C#控制台程序进行加壳和IL加密。
首先创建一个控制台应用,代码如下:
using System;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, world!");
Console.ReadKey();
}
}
}
生成Release版本,得到ConsoleApp.exe文件。
下载ConfuserEx的最新版本,解压缩后运行ConfuserEx.exe。
在ConfuserEx的界面上,点击左上角的"打开项目"按钮,选择"新项目"。项目名填写为"ConsoleApp",选择一个保存位置。
在项目设置的"混淆"选项卡里,勾选"启用混淆"。可以设置混淆的级别和一些规则。
切换到"封装"选项卡,勾选"启用压缩"和"启用高级混淆"。这会对IL代码进行加密。
点击左下角的"添加"按钮,选择之前生成的ConsoleApp.exe文件。
点击工具栏上的"生成"按钮,开始对程序集进行混淆加密,完成后会在项目目录下生成已保护的ConsoleApp.exe文件。
运行加壳后的ConsoleApp.exe,会发现仍然可以正常执行。但如果尝试使用一些反编译工具如ILSpy打开,会发现代码已经被加密了,无法还原出可读的C#代码。
以上就是使用ConfuserEx对一个简单的C#程序进行加壳和IL代码加密的过程。实际项目中可以根据需要调整保护的选项和规则。要注意的是,过度的加壳和加密可能会影响程序的性能,需要权衡安全性和运行效率。另外,没有绝对无法破解的保护,只是增加了破解的难度。需要与其他保护措施如代码混淆、反调试等结合使用,并且定期升级保护方法。
在.NET商业加密工具中,比较常用和评价较高的有以下几款:
Eazfuscator.NET
由Gapotchenko提供的商业.NET程序集保护工具
支持.NET Framework、.NET Core、Xamarin、Unity等平台
提供强大的混淆、加密、反调试、反反编译等功能
集成到Visual Studio中,使用方便
价格相对较高,但提供试用版和折扣
ProtectionPlus
由KSoftware提供的商业.NET程序集保护工具
支持.NET Framework、.NET Core、Xamarin、Unity等平台
提供代码混淆、控制流混淆、字符串加密、反调试等功能
支持自定义保护规则和插件扩展
价格相对较低,有多种许可方案可选
.NET Reactor
由Eziriz提供的商业.NET程序集保护工具
支持.NET Framework、.NET Core、Xamarin、Unity等平台
提供代码混淆、控制流混淆、资源压缩加密、反调试等功能
支持自定义保护规则和脚本
价格适中,提供永久许可和订阅许可
Crypto Obfuscator
由Logicnp提供的商业.NET程序集保护工具
支持.NET Framework、.NET Core、Xamarin、Unity等平台
提供代码混淆、字符串加密、控制流混淆、反调试等功能
支持自定义保护规则和插件扩展
价格相对较低,提供多种许可方案
Netz
由XenoCode提供的商业.NET程序集保护工具
支持.NET Framework和.NET Core平台
提供代码混淆、控制流混淆、字符串加密、反调试等功能
支持自定义保护规则和插件扩展
价格适中,提供永久许可和订阅许可
这些工具在功能、性能、易用性、价格等方面各有优势,适用于不同的项目和预算。它们都提供了对.NET程序集的强力保护,可以有效防止逆向工程和非法修改。
记一次.net加密神器 Eazfuscator.NET 2023.2 最新版 使用尝试
Eazfuscator.NET 很简单:
1.它可以保护您的代码,而不会破坏它 - 即使在最复杂的情况下 - 我们已经处理了它。 你可以把 Eazfuscator.NET 看作是一个很好的合作伙伴,他会帮你很多忙,但仍然不会因为他的任何问题而打扰你。 如果你有一些非常特殊的保护要求,注意 我们的代码虚拟化功能。
2.它真的很容易使用:一旦用 Eazfuscator.NET 保护你的Visual Studio项目,然后忘记它。 每次在发布配置中生成项目时,程序集都会自动进行模糊处理 - 您可以将代码发布到荒野中。
3.需要更改一些设置?是否确定?然后,做你喜欢的事情:编辑你的代码。Eazfuscator.NET 完全可配置使用 .NET 模糊处理属性 — 与使用外观陌生且不稳定的配置文件和 UI 向导相反。 您需要了解的所有信息都在我们完整的产品文档中。
混淆是什么样的?
‘
体验下Eazfuscator.NET功能
1、新创建winform.Eazfuscator.NET项目
框架我们选择.net framework 当然选择.netcore也可以的啦,最新版Eazfuscator.NET 2023.2 版本已经初步适配.net8
2023年4月30日更新
初步支持.NET 8.0
支持JetBrains Rider 2023.1
现在可以在类型/方法级别上控制代码控制流混淆
添加了一个配置设置,可以降低混淆过程的优先级
Eazfuscator.NET现在在ARM64机器上原生运行,无需触发x86模拟(适用于Windows 11+ ARM64、.NET Framework 4.8.1+)
更改的系统要求:Windows 8.1+,Windows Server 2012 R2+
改进了对.NET 7.0的支持
改进了程序集合并
改进了程序集嵌入
改进了NuGet集成
改进了元数据删除
改进了对各种序列化方案的支持
改进了文档
修复了可能导致"无法检索到XXX的自定义属性容器"错误的问题
修复了在混淆Unity项目时可能导致"找不到方法"错误的问题
修复了在运行混淆应用程序时可能导致"给定的程序集名称或代码库无效"错误的问题
修复了在混淆过程中可能导致"路径中有非法字符"错误的问题
修复了引用"GitVersion.MsBuild"包的项目在混淆过程中可能导致"依赖关系推断失败"错误的问题
修复了在编译过程中可能导致"标识符不符合CLS规范"的SGEN错误的问题
修复了在混淆时使用代码内联指令处理属性访问器时可能导致"属性具有属于另一个类型的相关方法"错误的问题
修复了在特定情况下合并程序集时可能导致"给定的键在字典中不存在"错误的问题
2、打开项目工程文件夹,找到csproj VS项目文件.
3、打开加密软件,并将此工程文件拖进去
现在我们查看它到底对创建的工程文件修改了什么
可以看到它为我们的工程文件添加了新的MSBuild属性,构建过程完成后执行加密操作。
<PropertyGroup>
<PostBuildEvent>if /I "$(ConfigurationName)" == "Release" Eazfuscator.NET.exe "$(TargetPath)" --msbuild-project-path "$(ProjectPath)" --msbuild-project-configuration "$(ConfigurationName)" --msbuild-project-platform "$(PlatformName)" --msbuild-solution-path "$(SolutionPath)" -n --newline-flush -v 2023.2</PostBuildEvent>
</PropertyGroup>
现在我们将项目切换到Release(发布)模式,编译我们的项目
点生成查看输出窗口发现它已经将发布的程序集加密了,是不是简单方便鸭?
现在我们使用反编译工具查看一下代码加密的前后对比
加密后可以看到所有关键的字符串及其方法都进行了混淆加密,这种混淆的代码难以让人读懂,有效保护了软件的知识产权。
以上就是我的一次代码加密小记录,适合新人小白,不熟悉Eazfuscator.NET加密的同学参考,简单通俗易懂。
微软工具ILMerge合并dll
ILMerge是一个可用于将多个.NET程序集合并为单个程序集的实用程序。
ILMerge接收一组输入程序集并将它们合并到一个目标程序集中。输入程序集列表中的第一个程序集是主程序集。
当主组件是可执行文件时,目标程序集将创建为与主程序集具有相同入口点的可执行程序。而且,如果主组件具有强名称,并提供了.snk文件,则使用指定的键重新签名目标程序集,以使其具有强名称。
操作
1.合并生成exe
ILMerge /log /target:winexe /out:sample.exe source\TestQuartz.exe source\Common.Logging.Core.dll
说明:
》/log:生成日志,可以忽略,控制台会输出log
》/target:生成输出类型,可以简写为/t
》/targetplatform:输出文件的.net运行版本,我这里没有使用,可以忽略
》/out:输出的文件名称,我是输出到当前ILMerge的根目录,如果需要输出到指定目录,请指定路径,例如我可以输出到 “D:\sample.exe”
》多个合并文件注意空格隔开,我这里资源放在了ILMerge目录下的source文件夹下,所以写法为 source\XXX
2.合并生成dll
ILMerge /log /target:dll /out:sample.dll source\NPOI.dll source\NPOI.OOXML.dll
上面已经介绍过就不多说了
C#图像视频人工智能处理库收集
除了AForge.NET之外,还有一些其他优秀的C#开源库用于图像处理、计算机视觉和机器学习。以下是几个值得关注的库:
OpenCvSharp:这是OpenCV计算机视觉库的C#包装器。OpenCV是一个功能强大、广泛使用的计算机视觉和机器学习库,OpenCvSharp允许在C#中使用OpenCV的各种功能。
Emgu CV:这是另一个OpenCV的C#包装器,提供了对OpenCV函数的访问,并且与.NET环境集成得很好。
Accord.NET:这是一个广泛的C#机器学习、计算机视觉和数值计算框架。它提供了大量的算法和工具,用于图像处理、统计分析、机器学习等任务。
DlibDotNet.NET:这是流行的C++机器学习库Dlib的C#包装器。它提供了一些先进的机器学习和计算机视觉算法,特别是在人脸识别和物体检测方面。
CNTK.NET:这是微软认知工具包(CNTK)的C#绑定。CNTK是一个深度学习框架,CNTK.NET允许在C#中使用CNTK的功能,如神经网络训练和推理。
TensorFlowSharp:这是谷歌TensorFlow深度学习框架的C#绑定。它允许在C#中使用TensorFlow的功能,如构建和训练神经网络模型。
Keras.NET:这是流行的Python深度学习库Keras的C#移植版。它提供了一个高级API,用于构建和训练神经网络模型,并与其他深度学习后端兼容,如TensorFlow和CNTK。
OpenCVSharp
使用VS的话,使用nuget进行搜索安装就好,个人建议安装OpenCvSharp3-AnyCPU
另一种方法呢就是去官网直接下载,然后引用“OpenCvSharp.dll”到依赖中
敲码OpenCVSharp
命名空间要引用:using OpenCvSharp;
例如(有点闲的慌):
//NameSpace: CVSharp
//FileName: CVSharp
//Create By: raink
//Create Time: 2019/9/27 14:24:08
using System;
using System.Collections.Generic;
using System.Text;
using OpenCvSharp;
namespace CVSharp
{
class CVSImage
{
private string path;
/// <summary>
/// 图像文件的全路径
/// </summary>
public string Path { get => path; set => path = value; }
private int width;
/// <summary>
/// 图像的宽
/// </summary>
public int Width { get => width; }
private int height;
/// <summary>
/// 图像的高
/// </summary>
public int Height { get => height; }
private long size;
/// <summary>
/// 图像的尺寸 size = width * height
/// </summary>
public long Size { get => size; }
/*-----------------------------------------------------------------*/
private Mat srcImg;
public CVSImage()
{
path = "";
width = 0;
height = 0;
size = 0;
srcImg = new Mat();
}
public CVSImage(string imagePath, int readMode=1)
{
path = imagePath;
srcImg = Cv2.ImRead(path, (ImreadModes)readMode);
width = srcImg.width;
height = srcImg.height;
size = width * height;
}
/*-----------------------------------------------------------------*/
/// <summary>
/// 预览图像
/// </summary>
/// <param name="winName">窗口名</param>
/// <param name="windowType">0-Normal(可调窗口尺寸),1-Autosize(图像原始大小)</param>
/// <param name="showTime">窗口停留时间(0则一直等待输入)</param>
/// <returns>异常返回-1,正常返回1</returns>
public int showImage(string winName, int windowType=1, int showTime=0)
{
if (srcImg == null || showTime < 0)
{
return -1;
}
WindowMode mode = (WindowMode)windowType;
Cv2.NamedWindow(winName, mode);
Cv2.ImShow(winName, srcImg);
Cv2.WaitKey(showTime);
return 0;
}
}
}
C#
Copy
然后再主函数里面调用一下:
using System;
using CVSharp;
namespace CVSharp
{
class Program
{
static void Main(string[] args)
{
CVSImage cvImage = new CVSImage("D:\\Asuna.jpg");
Console.WriteLine("image width: {0}", cvImage.Width);
Console.WriteLine("image height: {0}", cvImage.Height);
cvImage.showImage("Asuna", 0);
}
}
}
Emgu CV
Dlib.NET
要在Visual Studio 2022中使用Dlib.NET,你可以按照以下步骤进行:
安装DlibDotNet.NET:
在Visual Studio中打开"工具" -> "NuGet包管理器" -> "管理解决方案的NuGet程序包"。
在NuGet包管理器中,搜索"Dlib.NET"。
选择要安装Dlib.NET的项目,然后点击"安装"按钮。
配置项目:
在项目的属性页中,转到"生成"选项卡。
在"平台目标"下拉列表中,选择与你的系统架构相匹配的目标平台(例如x64)。
确保"允许不安全代码"选项被勾选,因为Dlib.NET使用了一些不安全的代码。
添加命名空间:
在你的C#代码文件中,添加以下命名空间:
using Dlib;
使用Dlib.NET提供的功能:
现在你可以在代码中使用Dlib.NET提供的各种功能,如人脸检测、人脸特征点检测等。
例如,以下代码演示了如何使用Dlib.NET进行人脸检测:
using System.Drawing;
using Dlib;
class Program
{
static void Main(string[] args)
{
// 加载图像
var img = Dlib.LoadImage<RgbPixel>("path/to/image.jpg");
// 加载人脸检测模型
var faceDetector = Dlib.GetFrontalFaceDetector();
// 检测人脸
var faces = faceDetector.Operator(img);
// 处理检测到的人脸
foreach (var face in faces)
{
// 在图像上绘制人脸边界框
Dlib.DrawRectangle(img, face, color: new RgbPixel(255, 0, 0), thickness: 2);
}
// 保存结果图像
Dlib.SaveJpeg(img, "path/to/output.jpg");
}
}
运行程序:
确保已经正确配置了项目并解决了任何编译错误。
运行程序并查看结果。
请注意,使用Dlib.NET可能需要一些额外的设置,如下载和配置相关的模型文件等。你可以参考Dlib.NET的官方文档和示例代码来了解更多详细信息。
总之,通过以上步骤,你应该能够在Visual Studio 2022中成功使用Dlib.NET,并利用它提供的强大功能进行机器学习和计算机视觉任务的开发
MaterialSkin for .NET WinForms
MaterialSkin 是一个用于 WinForms 应用程序的 C# 库,它提供了一组控件和组件,可以让你的应用程序具有 Google Material Design 的外观和感觉。这个库是开源的,托管在 GitHub 上。
以下是使用 MaterialSkin 库的一些关键步骤:
安装 MaterialSkin NuGet 包:
在 Visual Studio 的解决方案资源管理器中右击你的项目,选择 "管理 NuGet 程序包",搜索 "MaterialSkin",然后安装最新版本的 MaterialSkin 包。
引用 MaterialSkin 命名空间:
在你的窗体或控件的代码文件中,添加以下 using 语句来引用 MaterialSkin 命名空间:
using MaterialSkin;
using MaterialSkin.Controls;
初始化 MaterialSkinManager:
在你的主窗体的构造函数中,创建一个 MaterialSkinManager 的实例,并设置主题和颜色方案:
public partial class MainForm : MaterialForm
{
public MainForm()
{
InitializeComponent();
MaterialSkinManager materialSkinManager = MaterialSkinManager.Instance;
materialSkinManager.AddFormToManage(this);
materialSkinManager.Theme = MaterialSkinManager.Themes.LIGHT;
materialSkinManager.ColorScheme = new ColorScheme(Primary.BlueGrey800, Primary.BlueGrey900, Primary.BlueGrey500, Accent.LightBlue200, TextShade.WHITE);
}
}
使用 MaterialSkin 控件:
MaterialSkin 库提供了许多自定义控件,如 MaterialButton、MaterialCheckbox、MaterialListView 等。你可以在设计器中从工具箱拖放这些控件,或在代码中创建它们的实例。
以下是一个使用 MaterialSkin 按钮的简单示例:
MaterialButton materialButton1 = new MaterialButton();
materialButton1.Text = "Click Me";
materialButton1.Click += (sender, args) =>
{
MaterialMessageBox.Show("Button Clicked!");
};
this.Controls.Add(materialButton1);
这只是 MaterialSkin 库的基本介绍。该库还提供了许多其他功能和自定义选项,你可以探索其文档和示例项目以了解更多信息。
C#绿幕抠图
在C#中处理视频抠图通常涉及到视频处理和图像处理的技术。对于绿幕抠图,你需要使用一些图像处理库来帮助你完成这项工作。一个常用的库是 AForge.NET,但它可能不直接支持视频处理。更现代的选择可能是使用 FFmpeg 进行视频读取和写入,结合 ImageSharp 或 OpenCV 进行图像处理。
以下是一个基本的步骤概述以及一些示例代码片段,展示如何使用 OpenCV 和 FFmpeg.AutoGen 库来实现绿幕抠图的功能:
步骤概述:
安装所需的库 - 使用NuGet安装 OpenCvSharp4 和 FFmpeg.AutoGen。
读取视频 - 使用FFmpeg读取视频帧。
处理每一帧 - 对每一帧应用抠图算法。
写入新的视频文件 - 将处理后的帧写入新的视频文件。
示例代码:
首先确保你已经安装了所需的库:
sh
dotnet add package OpenCvSharp4
dotnet add package FFmpeg.AutoGen
然后可以编写如下C#代码:
csharp
using System;
using System.Drawing;
using OpenCvSharp;
using FFmpeg.AutoGen;
class Program
{
static void Main(string[] args)
{
// 视频文件路径
string inputFilePath = "path/to/input/video.mp4";
string outputFilePath = "path/to/output/video.mp4";
// 创建FFmpeg的AVFormatContext指针
using (var formatCtx = avformat.avformat_alloc_context())
{
// 打开输入文件
if (avformat.avformat_open_input(out formatCtx, inputFilePath, null, null) < 0)
{
Console.WriteLine("Could not open input file.");
return;
}
// 获取流信息
if (avformat.avformat_find_stream_info(formatCtx, IntPtr.Zero) < 0)
{
Console.WriteLine("Failed to retrieve input stream information");
return;
}
// 寻找视频流
int videoStreamIndex = -1;
for (int i = 0; i < formatCtx.nb_streams; i++)
{
if (formatCtx.streams[i].codecpar.codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
{
videoStreamIndex = i;
break;
}
}
if (videoStreamIndex == -1)
{
Console.WriteLine("No video stream found.");
return;
}
// 打开解码器
var codec = avcodec.avcodec_find_decoder((AVCodecID)formatCtx.streams[videoStreamIndex].codecpar.codec_id);
var codecCtx = avcodec.avcodec_alloc_context3(codec);
if (avcodec.avcodec_parameters_to_context(codecCtx, formatCtx.streams[videoStreamIndex].codecpar) < 0)
{
Console.WriteLine("Failed to copy codec context.");
return;
}
if (avcodec.avcodec_open2(codecCtx, codec, IntPtr.Zero) < 0)
{
Console.WriteLine("Failed to open codec.");
return;
}
// 初始化输出文件
var outFmtCtx = avformat.avformat_alloc_context();
if (avformat.avformat_alloc_output_context2(out outFmtCtx, null, null, outputFilePath) < 0)
{
Console.WriteLine("Failed to create output context.");
return;
}
// 复制视频流
var outStream = avformat.avformat_new_stream(outFmtCtx, codec);
avcodec.avcodec_parameters_copy(outStream.codecpar, formatCtx.streams[videoStreamIndex].codecpar);
// 写入文件头
avformat.avio_write_header(outFmtCtx.pb, IntPtr.Zero);
// 初始化帧
var frame = avcodec.av_frame_alloc();
// 开始处理每一帧
while (true)
{
// 读取一帧
var pkt = avformat.av_packet_alloc();
int ret = av_read_frame(formatCtx, pkt);
if (ret < 0)
{
break;
}
// 解码帧
ret = avcodec.avcodec_send_packet(codecCtx, pkt);
if (ret < 0)
{
Console.WriteLine("Error submitting a packet for decoding");
break;
}
ret = avcodec.avcodec_receive_frame(codecCtx, frame);
if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN))
{
continue;
}
else if (ret < 0)
{
Console.WriteLine("Error during decoding");
break;
}
// 转换帧到Mat
Mat mat = new Mat(frame.Height, frame.Width, MatType.CV_8UC3);
using (var sw = new Mat(frame.Linesize[0], frame.Height, MatType.CV_8UC1, frame.Data[0]))
{
Cv2.CvtColor(sw, mat, ColorConversionCodes.BGRA2BGR);
}
// 抠图处理
Mat mask = ExtractGreenScreen(mat);
// 合成背景
Mat result = ComposeWithBackground(mat, mask);
// 编码并写入帧
avcodec.avcodec_send_frame(codecCtx, frame);
avcodec.avcodec_receive_packet(codecCtx, pkt);
avformat.av_interleaved_write_frame(outFmtCtx, pkt);
// 清理
av_packet_unref(pkt);
av_packet_free(ref pkt);
}
// 写入文件尾
av_write_trailer(outFmtCtx);
// 清理资源
av_frame_free(ref frame);
avcodec.avcodec_close(codecCtx);
avformat.avformat_close_input(ref formatCtx);
avformat.avformat_free_context(outFmtCtx);
}
}
static Mat ExtractGreenScreen(Mat frame)
{
// 定义绿幕的范围
Scalar lowerGreen = new Scalar(50, 100, 50);
Scalar upperGreen = new Scalar(70, 255, 70);
// 转换颜色空间到HSV
Mat hsv = new Mat();
Cv2.CvtColor(frame, hsv, ColorConversionCodes.BGR2HSV);
// 创建掩模
Mat mask = new Mat();
Cv2.InRange(hsv, lowerGreen, upperGreen, mask);
// 去噪
Cv2.MorphologyEx(mask, mask, MorphTypes.Open, new Mat());
return mask;
}
static Mat ComposeWithBackground(Mat frame, Mat mask)
{
// 加载背景图片
Mat background = Cv2.ImRead("path/to/background.jpg", ImreadModes.Color);
// 调整背景大小与视频帧一致
background = Cv2.Resize(background, new Size(frame.Width, frame.Height), 0, 0, Inter.Linear);
// 应用掩模
Mat maskedFrame = new Mat();
Cv2.BitwiseAnd(frame, frame, maskedFrame, mask);
Mat maskedBackground = new Mat();
Cv2.BitwiseAnd(background, background, maskedBackground, mask.Scalar(0));
// 合成
Mat result = new Mat();
Cv2.Add(maskedFrame, maskedBackground, result);
return result;
}
}
请注意,上述代码仅作为示例,并未包含所有错误处理逻辑。实际使用时还需要根据具体需求进行调整和完善。
C#调用ffmpeg库AForge.Video.FFMPEG
在C#中控制ffplay界面,通常需要使用一个可以与ffplay交互的库
以下是使用AForge.Video.FFMPEG库控制ffplay界面的一个基本示例:
首先,安装AForge.Video.FFMPEG库:
Install-Package AForge.Video.FFMPEG
然后,你可以使用以下代码来控制ffplay界面:
using AForge.Video;
using AForge.Video.FFMPEG;
class FFplayController
{
private FFmpegStream stream;
private VideoFileReader videoPlayer;
public void PlayVideo(string filePath)
{
stream = new FFmpegStream(filePath);
videoPlayer = new VideoFileReader();
videoPlayer.Open(stream);
// 播放视频
videoPlayer.Play();
// 暂停视频
// videoPlayer.Pause();
// 停止视频
// videoPlayer.Stop();
// 调整音量
// videoPlayer.Volume = 0.5;
// 获取视频帧并处理
// videoPlayer.NewFrame += VideoPlayer_NewFrame;
}
// 处理视频帧的事件
// private void VideoPlayer_NewFrame(object sender, ref Bitmap image)
// {
// // 对图像进行处理
// }
}
请注意,FFmpegStream和VideoFileReader类是AForge.Video.FFMPEG提供的,用于处理视频流。你可以使用它们来控制播放、暂停、停止以及调整音量等操作。
在C#中使用libVLC进行媒体播放
libVLC是VLC媒体播放器的核心库,提供了一个跨平台的框架来播放和流式传输多种多媒体格式的内容。通过使用libVLC,开发者可以在自己的应用程序中实现视频播放和其他媒体相关的功能。本文将介绍如何在C#应用程序中集成和使用libVLC。
确保你的开发环境已经安装了.NET Framework或.NET Core,并且已经安装了Visual Studio。接下来,你需要安装libVLC的C#绑定——LibVLCSharp。
安装LibVLCSharp
选择“管理NuGet包”。在“浏览”选项卡中搜索LibVLCSharp并安装它
包管理器控制台:打开“工具” > “NuGet包管理器” > “包管理器控制台”,然后运行以下命令:
Install-Package LibVLCSharp
1. 添加LibVLCSharp控件
首先,在你的WPF窗口的XAML中添加VideoView控件。确保你的XAML顶部有正确的命名空间引用:
<Window x:Class="YourNamespace.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lvs="clr-namespace:LibVLCSharp.WPF;assembly=LibVLCSharp.WPF"
Title="MainWindow" Height="450" Width="800">
<Grid>
<lvs:VideoView x:Name="videoView" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</Grid>
</Window>
2. 初始化LibVLC和MediaPlayer
在你的窗口后台代码中,初始化LibVLC对象和MediaPlayer对象,并将MediaPlayer绑定到VideoView控件:
using LibVLCSharp.Shared;
public partial class MainWindow : Window
{
private LibVLC _libVLC;
private MediaPlayer _mediaPlayer;
public MainWindow()
{
InitializeComponent();
Core.Initialize();
_libVLC = new LibVLC();
_mediaPlayer = new MediaPlayer(_libVLC)
{
Media = new Media(_libVLC, "your_video_path_here", FromType.FromPath)
};
videoView.MediaPlayer = _mediaPlayer;
}
}
3. 播放视频
最后,你可以通过调用Play方法来播放视频:
public void PlayVideo()
{
_mediaPlayer.Play();
}
确保在适当的时机(例如窗口加载完成后)调用PlayVideo方法,如果窗口没加载完就调用了Play方法,则会出现播放器弹出播放。
错误处理和日志
处理媒体播放中可能遇到的错误是很重要的。LibVLCSharp允许你注册日志监听器和事件处理器来处理播放过程中的错误和状态变化:
_libVLC.Log += (sender, e) =>
{
Console.WriteLine($"[LibVLC Log] {e.Level}: {e.Message}");
};
_mediaPlayer.EncounteredError += (sender, e) =>
{
MessageBox.Show("播放过程中遇到错误。");
};
清理资源
在应用程序关闭或不再需要播放器时,正确清理资源是很重要的。你应该释放MediaPlayer和LibVLC实例:
public void Cleanup()
{
_mediaPlayer.Stop();
_mediaPlayer.Dispose();
_libVLC.Dispose();
}
确保在窗口关闭事件或应用程序退出时调用Cleanup方法。
FFplay成功地移动到我的Winform中,如何将其设置为无边框
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Diagnostics;
using System.Threading;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Drawing.Text;
using System.Text.RegularExpressions;
using System.Configuration;
using Microsoft.Win32;
using System.Windows.Forms.VisualStyles;
namespace xFFplay
{
public partial class Form1 : Form
{
[DllImport("user32.dll", SetLastError = true)]
private static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
[DllImport("user32.dll")]
private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
//Process ffplay = null;
public Form1()
{
InitializeComponent();
Application.EnableVisualStyles();
this.DoubleBuffered = true;
}
public Process ffplay = new Process();
private void xxxFFplay()
{
// start ffplay
/*var ffplay = new Process
{
StartInfo =
{
FileName = "ffplay.exe",
Arguments = "Revenge.mp4",
// hides the command window
CreateNoWindow = true,
// redirect input, output, and error streams..
//RedirectStandardError = true,
RedirectStandardOutput = true,
UseShellExecute = false
}
};
* */
//public Process ffplay = new Process();
ffplay.StartInfo.FileName = "ffplay.exe";
ffplay.StartInfo.Arguments = "Revenge.mp4";
ffplay.StartInfo.CreateNoWindow = true;
ffplay.StartInfo.RedirectStandardOutput = true;
ffplay.StartInfo.UseShellExecute = false;
ffplay.EnableRaisingEvents = true;
ffplay.OutputDataReceived += (o, e) => Debug.WriteLine(e.Data ?? "NULL", "ffplay");
ffplay.ErrorDataReceived += (o, e) => Debug.WriteLine(e.Data ?? "NULL", "ffplay");
ffplay.Exited += (o, e) => Debug.WriteLine("Exited", "ffplay");
ffplay.Start();
Thread.Sleep(500); // you need to wait/check the process started, then...
// child, new parent
// make 'this' the parent of ffmpeg (presuming you are in scope of a Form or Control)
SetParent(ffplay.MainWindowHandle, this.Handle);
// window, x, y, width, height, repaint
// move the ffplayer window to the top-left corner and set the size to 320x280
MoveWindow(ffplay.MainWindowHandle, 0, 0, 320, 280, true);
}
private void button1_Click(object sender, EventArgs e)
{
xxxFFplay();
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
try { ffplay.Kill(); }
catch { }
}
}
}
ffplay允许使用-noborder选项的无边界窗
更改
ffplay.StartInfo.Arguments = "-noborder Revenge.mp4";
Flaui查找指定类名和标题的窗口代码
Install-Package FlaUI.Core
using FlaUI.Core;
using FlaUI.Core.AutomationElements;
using FlaUI.Core.Conditions;
using FlaUI.Core.Input;
using FlaUI.Core.Tools;
using System;
class Program
{
static void Main(string[] args)
{
try
{
// 启动应用程序(如果需要)
// Process.Start("你的应用程序路径");
// 等待应用程序启动
var app = Application.AttachOrLaunch("你的应用程序名称", "应用程序启动参数");
// 使用FlaUI提供的Condition来查找具有特定类名和标题的窗口
var windowCondition = Conditions.WindowTitle("你的窗口标题")
.And(Conditions.WindowClassName("你的窗口类名"));
var window = app.GetWindow(windowCondition);
if (window != null)
{
Console.WriteLine("找到窗口:" + window.Title);
// 接下来你可以使用 window 对象来进一步操作,例如:
// window.Close(); // 关闭窗口
// window.Minimize(); // 最小化窗口
// window.Maximize(); // 最大化窗口
// 或者获取窗口内的元素进行操作
}
else
{
Console.WriteLine("未找到窗口");
}
}
catch (Exception ex)
{
Console.WriteLine("发生错误:" + ex.Message);
}
}
}
请将 "你的应用程序名称" 和 "你的窗口标题" 替换为实际的应用程序名称和窗口标题。如果你的应用程序需要启动参数,也可以将 "应用程序启动参数" 替换为实际的参数。
这段代码首先尝试启动应用程序(如果它还没有运行),然后使用 FlaUI 的条件 API 创建一个窗口查找条件,该条件包括窗口的标题和类名。然后,它使用这个条件来查找窗口,并在找到窗口时执行一些操作。
Redis数据库缓存key设计
缓存表的key设计
mysql> select * from login;
+---------+----------------+-------------+---------------------+
| user_id | name | login_times | last_login_time |
+---------+----------------+-------------+---------------------+
| 1 | ken thompson | 5 | 2011-01-01 00:00:00 |
| 2 | dennis ritchie | 1 | 2011-02-01 00:00:00 |
| 3 | Joe Armstrong | 2 | 2011-03-01 00:00:00 |
+---------+----------------+-------------+---------------------+
一般使用冒号做分割符,这是不成文的规矩。比如在php-admin for redis系统里,就是默认以冒号分割,于是user:1 user:2等key会分成一组。于是以上的关系数据转化成kv数据后记录如下:
Set login:1:login_times 5
Set login:2:login_times 1
Set login:3:login_times 2
Set login:1:last_login_time 2011-1-1
Set login:2:last_login_time 2011-2-1
Set login:3:last_login_time 2011-3-1
set login:1:name ”ken thompson“
set login:2:name “dennis ritchie”
set login:3:name ”Joe Armstrong“
key=表名:主键值:字段名 字段值
在设计 Redis 缓存的键时,以下是一些合理的设计原则和建议:
唯一性:确保键在缓存中是唯一的,以避免键冲突和数据混淆。通常,可以将键与缓存项的关键属性或标识相关联
可读和可理解性:选择具有可读性和可理解性的键名称,以便开发人员能够轻松理解键所代表的含义和关联的数据。
一致性:在整个应用程序中保持一致的键命名约定,以便在不同的代码段中使用相同的键。这有助于降低代码维护的复杂性。
简洁性:避免使用过长或冗余的键名,以减小存储和传输开销。
下面是一些常见的键设计模式示例:
a. 基于对象标识的键设计:
string key = $"user:{userId}"; // 例如:user:123
b. 基于对象类型和标识的键设计:
string key = $"user:{userId}:profile"; // 例如:user:123:profile
c. 基于操作和参数的键设计:
string key = $"product:{productId}:reviews"; // 例如:product:456:reviews
d. 使用命名空间的键设计:
string key = $"cache:{namespace}:{keyName}"; // 例如:cache:users:123
e. 使用版本控制的键设计:
string key = $"user:{userId}:v2"; // 例如:user:123:v2
这些只是一些常见的键设计模式示例,具体的键设计应该根据你的应用程序的需求和数据模型来进行选择和调整。
此外,还应注意以下事项:
键的长度:确保键的长度不会过长,以避免网络传输和存储的开销。
键的命名空间:在键的设计中使用适当的命名空间,以避免键冲突和数据泄漏。
键的过期时间:根据缓存数据的特性和数据更新频率,设置适当的过期时间,以避免存储过期的数据。
使用 Redis 缓存 SQL 查询的数据库表数据,你可以按照以下步骤进行操作:
在应用程序中执行 SQL 查询,获取数据库表的数据结果集。
将获取到的数据结果集存储到 Redis 中。你可以使用 Redis 的数据类型来存储不同的数据结构:
如果数据结果集是一个简单的键值对集合,你可以使用 Redis 的哈希表(Hash)数据类型。将表的每一行作为哈希表的一个字段(field),并将对应的字段值(value)设置为该行的数据。可以使用 Redis 的命令如 HSET、HMSET 等来设置哈希表。
如果数据结果集是一个列表(List),你可以使用 Redis 的列表(List)数据类型。将每一行数据作为列表中的一个元素,按照查询结果的顺序存储。可以使用 Redis 的命令如 LPUSH、RPUSH 等来添加元素到列表中。
如果数据结果集是一个集合(Set),你可以使用 Redis 的集合(Set)数据类型。将每一行数据作为集合中的一个成员,确保成员的唯一性。可以使用 Redis 的命令如 SADD 来添加成员到集合中。
如果数据结果集是一个有序集合(Sorted Set),你可以使用 Redis 的有序集合(Sorted Set)数据类型。将每一行数据作为有序集合中的一个成员,可以根据某个字段的值来进行排序。可以使用 Redis 的命令如 ZADD 来添加成员到有序集合中。
在需要使用缓存数据的时候,首先检查 Redis 中是否存在对应的缓存数据。可以使用 Redis 的命令如 HGET、HGETALL 获取哈希表数据,使用 LRANGE 获取列表数据,使用 SMEMBERS 获取集合数据,使用 ZRANGE 获取有序集合数据等。
如果缓存数据存在,则直接使用 Redis 中的数据。如果缓存数据不存在,则从数据库中重新获取数据,并将数据存储到 Redis 中,以供下次使用。
需要注意的是,当数据库中的数据发生变化时,你需要及时更新 Redis 中的缓存数据,以保持数据的一致性。可以在数据库更新操作完成后,清除对应的缓存数据,或者更新缓存数据的内容。
这是一种基本的方法来缓存 SQL 查询的数据库表数据。具体的实现方式可能因应用程序的需求和数据模型的复杂性而有所不同。
当使用 C# 编程语言时,你可以使用 StackExchange.Redis 库来与 Redis 进行交互。下面是一个使用 C# 进行 Redis 缓存的 SQL 查询结果的简单示例代码:
using StackExchange.Redis;
using System;
using System.Data;
using System.Data.SqlClient;
public class RedisCacheExample
{
private readonly ConnectionMultiplexer _redisConnection;
public RedisCacheExample()
{
// 创建 Redis 连接
_redisConnection = ConnectionMultiplexer.Connect("localhost");
}
public DataTable GetTableDataFromCache(string cacheKey)
{
IDatabase redisCache = _redisConnection.GetDatabase();
// 检查 Redis 中是否存在缓存数据
if (redisCache.KeyExists(cacheKey))
{
// 从 Redis 中获取缓存数据
RedisValue[] cachedData = redisCache.HashValues(cacheKey);
// 将缓存数据转换为 DataTable
DataTable dataTable = ConvertToDataTable(cachedData);
return dataTable;
}
else
{
// 从数据库中获取数据
DataTable dataTable = GetDataFromDatabase();
// 将数据存储到 Redis 中
SaveDataToCache(cacheKey, dataTable);
return dataTable;
}
}
private DataTable GetDataFromDatabase()
{
// 连接数据库,执行 SQL 查询,并获取数据结果集
string connectionString = "your_connection_string";
string query = "SELECT * FROM YourTable";
using (SqlConnection connection = new SqlConnection(connectionString))
{
using (SqlCommand command = new SqlCommand(query, connection))
{
connection.Open();
SqlDataAdapter adapter = new SqlDataAdapter(command);
DataTable dataTable = new DataTable();
adapter.Fill(dataTable);
return dataTable;
}
}
}
private void SaveDataToCache(string cacheKey, DataTable dataTable)
{
IDatabase redisCache = _redisConnection.GetDatabase();
// 将 DataTable 转换为 Redis 哈希表
HashEntry[] hashEntries = ConvertToHashEntries(dataTable);
// 存储哈希表到 Redis 中
redisCache.HashSet(cacheKey, hashEntries);
}
private DataTable ConvertToDataTable(RedisValue[] cachedData)
{
DataTable dataTable = new DataTable();
// 假设 Redis 哈希表的字段名就是数据库表的列名
foreach (RedisValue field in cachedData)
{
dataTable.Columns.Add(field.ToString());
}
// 假设 Redis 哈希表的字段值就是数据库表的行数据
DataRow dataRow = dataTable.NewRow();
for (int i = 0; i < cachedData.Length; i++)
{
dataRow[i] = cachedData[i];
}
dataTable.Rows.Add(dataRow);
return dataTable;
}
private HashEntry[] ConvertToHashEntries(DataTable dataTable)
{
HashEntry[] hashEntries = new HashEntry[dataTable.Columns.Count];
for (int i = 0; i < dataTable.Columns.Count; i++)
{
string fieldName = dataTable.Columns[i].ColumnName;
string fieldValue = dataTable.Rows[0][i].ToString();
hashEntries[i] = new HashEntry(fieldName, fieldValue);
}
return hashEntries;
}
}
以上是一个简单的示例代码,演示了如何使用 C# 通过 StackExchange.Redis 库与 Redis 进行交互,将 SQL 查询的结果集存储到 Redis 缓存中并从缓存中获取数据。请注意,示例中的连接字符串和查询语句需要根据你的实际情况进行修改。
IRedisClient操作
一、属性
IRedisClient的属性如下:
属性 说明
ConnectTimeout 连接超时
Db 当前数据库的ID或下标
DbSize 当前数据库的 key 的数量
HadExceptions
Hashes 存储复杂对象,一个value中有几个field
Host Redis的Server服务器主机地址
Info 返回关于 Redis 服务器的各种信息和统计数值
LastSave 最近一次 Redis 成功将数据保存到磁盘上的时间
Lists 当前数据库中所有的List集合
Password 密码
Port Redis的Server端口
RetryCount 重试次数
RetryTimeout 重试超时
SendTimeout 发送超时
Sets 当前数据库中所有的HashSet<T>集合
SortedSets 当前数据库中所有的SortedSet<T>集合
this[string key] 通过索引的方式(key)访问一个字符串类型值
代码示例:
复制代码
RClient.AddItemToSet("蜀国", "刘备");
RClient.AddItemToSet("蜀国", "关羽");
RClient.AddItemToSet("蜀国", "张飞");
IHasNamed<IRedisSet> rr = RClient.Sets;
HashSet<string> HashSetString = rr["蜀国"].GetAll();
foreach (string str in HashSetString)
{
Response.Write(str);
}
二、IRedisClient数据操作
1、ICacheClient接口
IRedisClient实现了接口ICacheClient,其中ICacheClient主要提供的功能如下:
方法 说明
Add 根据传入的key-value添加一条记录,当key已存在返回false
FlushAll 使所有缓存失效(清除Redis所有数据库的所有Key)
Get 根据传入的key获取一条记录的值
GetAll 根据传入的多个key获取多条记录的值
Remove 根据传入的key移除一条记录
RemoveAll 根据传入的多个key移除多条记录
Replace 根据传入的key覆盖一条记录的值,当key不存在不会添加
Set 根据传入的key修改一条记录的值,当key不存在则添加
SetAll 根据传入的多个key覆盖多条记录
Increment
Decrement
public ActionResult Index()
{
RedisClientManagerConfig RedisConfig = new RedisClientManagerConfig();
RedisConfig.AutoStart = true;
RedisConfig.MaxReadPoolSize = 60;
RedisConfig.MaxWritePoolSize = 60;
PooledRedisClientManager prcm = new PooledRedisClientManager(new List<string>() { "127.0.0.1" }, new List<string>() { "127.0.0.1" }, RedisConfig);
using (IRedisClient RClient = prcm.GetClient())
{
RClient.Add("c1", "缓存1");
RClient.Set("c1", "缓存2");
RClient.Replace("c1", "缓存3");
Response.Write(RClient.Get<string>("c1"));
RClient.Remove("c1");
Response.Write(RClient.Get<string>("c1") == null);
}
return Content("");
}
2、简单功能
当然,除了实现ICacheClient接口的功能外,对于基本操作,实际上也还有很多功能
方法 说明
AppendToValue 根据Key将参数value追加到原有值的结尾
ContainsKey 判断Key在本数据库内是否已被使用(包括各种类型、内置集合等等)
GetAllKeys 获取所有的Keys集合
DecrementValue 根据指定的Key,将值减1(仅整型有效)
DecrementValueBy 根据指定的Key,将值减去指定值(仅整型有效)
IncrementValue 根据指定的Key,将值加1(仅整型有效)
IncrementValueBy 根据指定的Key,将值加上指定值(仅整型有效)
RenameKey 重命名一个Key,值不变
SearchKeys 从数据库中查找名称相等的Keys的集合,特殊模式如h[ae]llo,仅英文有效。
GetRandomKey 随机获取一个已经被使用的Key
GetValue 根据Key获取值,只对string类型有效
GetValues 根据输入的多个Key获取多个值,支持泛型
GetTimeToLive 获取指定Key的项距离失效点的TimeSpan
GetSortedSetCount 获取已排序集合的项的数目,参数支持下标以及score筛选
ExpireEntryAt 根据指定的key设置一项的到期时间(DateTime)
ExpireEntryIn 根据指定的key设置一项的到期时间(TimeSpan)
FlushDb 清除本数据库的所有数据
FlushAll 清除所有数据库的所有数据
Shutdown 停止所有客户端,保存,关闭Redis服务
Save 保存数据DB文件到硬盘
SaveAsync 异步保存
RewriteAppendOnlyFileAsync 只在异步情况下将数据追加到服务器文件
WriteAll
PublishMessage 将Message发送到指定的频道
StoreObject
GetValuesMap 以键值对的方式返回值类型相同的多条数据,支持泛型与返回字符串。
字符串
SetEntry 根据Key修改一个值,存在则覆盖。(只能设置字符串)
SetEntryIfNotExists 根据Key设置一个值,仅仅当Key不存在时有效,如Key已存在则不修改(只支持字符串)
SetEntryIfNotExists 根据Key设置一个值,返回旧值。
GetEntryType 根据Key获取当前存储的值是什么类型:
None = 0 String = 1 List = 2 Set = 3 SortedSet = 4 Hash = 5
3、内置集合
比如,IRedisClient支持在内部维护如下集合类型的数据:
List<T>
排序的List<T>(.Net 4.0后的SortedSet)
HashSet<T>
关于如下4种类型数据的操作:
方法 说明
AddItemToList 添加一个项到内部的List<T>
AddItemToSet 添加一个项到内部的HashSet<T>
AddItemToSortedSet 添加一个项到内部的排序List<T>,其中重载方法多了个score:排序值。优先按照score从小->大排序,否则按值小到大排序
AddRangeToList 一次过将参数中的List<T>中的多个值添加入内部的List<T>
AddRangeToSet 一次过将参数中的HashSet<T>中的多个值添加入内部的HashSet<T>
AddRangeToSortedSet 一次过将参数中的List<T>中的多个值添加到内部List<T>,重载方法的score表示排序值。
GetAllItemsFromList 获取指定ListId的内部List<T>的所有值
GetAllItemsFromSet 获取指定SetId的内部HashSet<T>的所有值
GetAllItemsFromSortedSet 获取指定ListId的内部已排序List<T>的所有值
GetAllItemsFromSortedSetDesc 获取指定ListId的内部已排序List<T>的所有值,不过获取的值是倒序排列后的。
GetRangeFromList 获取指定ListId的内部List<T>中指定下标范围的数据
GetRangeFromSortedList 获取指定ListId的内部已排序List<T>中指定下标范围的数据
GetRangeFromSortedSet 获取指定SetId的内部HashSet<T>中指定下标范围的数据
GetRangeFromSortedSetByHighestScore 获取指定SetId的内部HashSet<T>中按照score由高->低排序后的分值范围的数据,并且支持skip、take
GetRangeFromSortedSetByLowestScore 同上,只不过是按score分值由低->高取一定范围内的数据
GetRangeFromSortedSetDesc 按倒序获取内部HashSet<T>的指定下标范围内的数据
GetRangeWithScoresFromSortedSet 与From相同,只不过获取的是键值对,数据中带分值score
GetRangeWithScoresFromSortedSetByHighestScore 同上
GetRangeWithScoresFromSortedSetByLowestScore 同上
GetRangeWithScoresFromSortedSetDesc 同上
GetAllWithScoresFromSortedSet 获取指定ListId的已排序的内部List<T>与其score
GetSortedItemsFromList 从指定ListId的List<T>中获取按指定排序的集合,支持Skip,Take
GetSortedEntryValues 从指定ListId的List<T>中获取经过排序指定开始位置与个数的项
RemoveAllFromList 移除指定ListId的内部List<T>
RemoveItemFromList 移除指定ListId的内部List<T>中第二个参数值相等的那一项
RemoveItemFromSet 从指定SetId的内部HashSet<T>中移除与第二个参数值相等的那一项
RemoveItemFromSortedSet 从指定ListId中已排序的内部List<T>中移除值相等的那一项
RemoveRangeFromSortedSet 从指定ListId已排序的List<T>中移除指定下标范围的项
RemoveRangeFromSortedSetByScore 从指定ListId已排序的List<T>中移除指定score范围的项
RemoveStartFromList 从指定ListId移除开头那一项
RemoveEndFromList 从指定ListId移除末尾那项
BlockingRemoveStartFromList 阻塞地从指定ListId移除开头那一项
BlockingRemoveStartFromLists
RemoveEntry 根据传入的多个ListId,清除多个内部List<T>
RemoveAllLuaScripts 清除所有的 Lua 脚本缓存
RemoveEntryFromHash
GetItemFromList 根据ListId和下标获取一项
GetItemIndexInSortedSet 根据List和值,获取内置的排序后的List<T>的下标
GetItemIndexInSortedSetDesc 同上,不过顺序相反
GetItemScoreInSortedSet 根据传入的ListId和值获取内置List<T>项的score
GetListCount 根据ListId,获取内置的List<T>的项数
GetSetCount 根据SetId,获取内置的HashSet<T>的项数
GetIntersectFromSets 从输入的多个HashSet<T>的Id中获取交集
GetUnionFromSets 从输入的多个HashSet<T>的Id中获取并集
GetRandomItemFromSet 从指定ListId的集合中获取随机项
StoreUnionFromSets 将多个HashSet<T>,合并为第一个参数中的一个大HashSet<T>,第一个参数中的HashSet<T>原本可以不存在
StoreUnionFromSortedSets 将多个SortedSet<T>,合并为第一个参数中的一个大SortedSet<T>,第一个参数中的SortedSet<T>原本可以不存在
StoreIntersectFromSets 将交集结果保存在第一个参数的集合中,对HastSet<T>作用
StoreIntersectFromSortedSets 将交集结果保存在第一个参数的集合中,对SortedSet<T>作用
EnqueueItemOnList 将一个元素存入指定ListId的List<T>的头部
DequeueItemFromList 将指定ListId的List<T>末尾的那个元素出列,返回出列元素
BlockingDequeueItemFromList 将指定ListId的List<T>末尾的那个元素出列,区别是:会阻塞该List<T>,支持超时时间,返回出列元素
BlockingDequeueItemFromLists
BlockingPopItemFromList 阻塞地将指定ListId的List<T>末尾的哪一个元素移除
BlockingPopItemFromLists
BlockingPopAndPushItemBetweenLists 将第一个集合的元素移除并添加到第二个集合的头部,返回该元素,会同时阻塞两个集合
PopItemFromList 从指定ListId的List<T>末尾移除一项并返回
PopItemFromSet 从指定SetId的HashSet<T>末尾移除一项并返回
PopItemWithHighestScoreFromSortedSet 从指定SetId的HashSet<T>移除score最高的那一项
PopItemWithLowestScoreFromSortedSet 从指定SetId的HashSet<T>移除score最低的那一项
PopAndPushItemBetweenLists 将第一个集合的元素移除并添加到第二个集合的头部
SetContainsItem 判断指定SetId的HashSet<T>中是否包含指定的value(仅仅支持字符串)
SortedSetContainsItem 判断SortedSet是否包含一个键
TrimList 根据ListId裁剪内置集合,保留下去from->at之间(包含from于at)的元素,其余的裁去
IncrementItemInSortedSet 为指定ListId的集合中的value的分值score加上指定分值
SetItemInList 重新设置指定ListId和下标的value为指定值
PushItemToList 在指定ListId的内置List<T>中入列一个键值对,在末尾
PrependItemToList 将一个值插入到List<T>的最前面
PrependRangeToList 一次性添加多个值到指定ListId的内置List<T>中
GetDifferencesFromSet 返回存在于第一个集合,但是不存在于其他集合的数据。差集
StoreDifferencesFromSet 将求差集的结果保存在第一个参数的集合中
MoveBetweenSets 将元素从一个集合移动到另一个集合的开头。(删除与添加)
下面仅给出一个List<T>与HashSet<T>的示例:
//内部维护一个List<T>集合
RClient.AddItemToList("蜀国", "刘备");
RClient.AddItemToList("蜀国", "关羽");
RClient.AddItemToList("蜀国", "张飞");
List<string> ListString = RClient.GetAllItemsFromList("蜀国");
foreach (string str in ListString)
{
Response.Write(str); //输出 刘备 关羽 张飞
}
RClient.AddItemToSet("魏国", "曹操");
RClient.AddItemToSet("魏国", "曹操");
RClient.AddItemToSet("魏国", "典韦");
HashSet<string> HashSetString = RClient.GetAllItemsFromSet("魏国");
foreach (string str in HashSetString)
{
Response.Write(str); //输出 典韦 曹操
}
下面再给一个范围Range操作示例:
//内部维护一个List<T>集合
RClient.AddItemToSortedSet("蜀国", "刘备", 5);
RClient.AddItemToSortedSet("蜀国", "关羽", 2);
RClient.AddItemToSortedSet("蜀国", "张飞", 3);
IDictionary<String,double> DicString = RClient.GetRangeWithScoresFromSortedSet("蜀国", 0, 2);
foreach (var r in DicString)
{
Response.Write(r.Key + ":" + r.Value); //输出
}
3、内置Hash
内部维护一个HashTable
方法 说明
SetEntryInHash 设置一个键值对入Hash表,如果哈希表的key存在则覆盖
SetEntryInHashIfNotExists 当哈希表的key未被使用时,设置一个键值对如Hash表
GetHashValues 根据HashId获取多个改HashId下的多个值
GetValuesFromHash 根据HashId和Hash表的Key获取多个值(支持多个key)
GetValueFromHash 根据HashId和Hash表的Key获取单个值
GetHashKeys 获取指定HashId下的所有Key
GetHashValues 获取指定HashId下的所有值
GetHashCount 获取指定HashId下的所有Key数量
HashContainsEntry 判断指定HashId的哈希表中是否包含指定的Key
IncrementValueInHash 将指定HashId的哈希表中的值加上指定值
StoreAsHash 将一个对象存入Hash(支持泛型)
GetFromHash 根据Id从Hash表中取出对象(支持泛型)
SetRangeInHash 通过IEnumerable<KeyValuePair<string, string>>一次性设置多个值,当内部Hash的key不存在则添加,存在则覆盖
代码示例:
RClient.SetEntryInHash("xxx","key","123");
List<KeyValuePair<string, string>> keyValuePairs = new List<KeyValuePair<string, string>>();
KeyValuePair<string, string> kvp = new KeyValuePair<string, string>("key", "1");
keyValuePairs.Add(kvp);
RClient.SetRangeInHash("xxx", keyValuePairs);
4、Lua Script
从 Redis 2.6.0 版本开始,通过内置的 Lua 解释器,可以执行各种Lua脚本。IRedisClient支持执行Lua脚本,其供用于执行Lua脚本的方法如下:
方法 说明
LoadLuaScript 将一个脚本装入脚本缓存,但并不立即运行它
KillRunningLuaScript 停止正在运行的指定Id的脚本
ExecLuaAsInt
ExecLuaAsList
ExecLuaAsString
ExecLuaShaAsInt
ExecLuaShaAsList
ExecLuaShaAsString
HasLuaScript 判断Lua脚本是否在脚本缓存里
CalculateSha1
WhichLuaScriptsExists
关于Lua脚本可以到这里去了解:http://www.cnblogs.com/ly4cn/archive/2006/08/04/467550.html
5、事务
Redis中的事务
方法 说明
Watch 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
UnWatch 取消 WATCH 命令对所有 key 的监视
AcquireLock 申请对一个Key加锁(期间其他对象不能访问)
CreateTransaction 创建一个事务,返回一个IRedisTransaction对象
CreateSubscription 创建一个订阅事件返回一个IRedisSubscription对象
CreatePipeline 返回一个IRedisPipeline对象
C# websocket服务端客户端的详细介绍及例子
C#中的WebSocket服务端和客户端,并提供一些例子。
WebSocket是一种在单个TCP连接上进行全双工通信的协议。它允许服务器主动向客户端推送数据,这在实时应用程序中非常有用。
首先,让我们看看如何创建一个WebSocket服务端:
WebSocket服务端
在C#中,我们可以使用System.Net.WebSockets命名空间中的类来实现WebSocket服务端。这里是一个简单的例子:
using System;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
var listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8080/");
listener.Start();
Console.WriteLine("Server started...");
while (true)
{
HttpListenerContext context = await listener.GetContextAsync();
if (context.Request.IsWebSocketRequest)
{
HttpListenerWebSocketContext webSocketContext = await context.AcceptWebSocketAsync(null);
WebSocket webSocket = webSocketContext.WebSocket;
await HandleWebSocketConnection(webSocket);
}
}
}
static async Task HandleWebSocketConnection(WebSocket webSocket)
{
var buffer = new byte[1024 * 4];
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
while (!result.CloseStatus.HasValue)
{
string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine($"Received: {message}");
string response = $"Server: {message}";
byte[] responseBytes = Encoding.UTF8.GetBytes(response);
await webSocket.SendAsync(new ArraySegment<byte>(responseBytes), WebSocketMessageType.Text, true, CancellationToken.None);
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
}
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}
}
这个服务端监听在localhost:8080上,接受WebSocket连接,然后echo回客户端发送的消息。
WebSocket客户端
现在,让我们看看如何创建一个WebSocket客户端:
using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
using (ClientWebSocket ws = new ClientWebSocket())
{
Uri serverUri = new Uri("ws://localhost:8080/");
await ws.ConnectAsync(serverUri, CancellationToken.None);
Console.WriteLine("Connected to server");
while (ws.State == WebSocketState.Open)
{
Console.Write("Enter message: ");
string message = Console.ReadLine();
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
await ws.SendAsync(new ArraySegment<byte>(messageBytes), WebSocketMessageType.Text, true, CancellationToken.None);
byte[] receiveBuffer = new byte[1024];
WebSocketReceiveResult result = await ws.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
string receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, result.Count);
Console.WriteLine($"Received: {receivedMessage}");
}
}
}
}
}
这个客户端连接到ws://localhost:8080/,允许用户输入消息并发送到服务器,然后接收并显示服务器的响应。
使用这些例子,你可以建立一个基本的WebSocket通信系统。在实际应用中,你可能需要添加更多的错误处理,支持更复杂的消息格式(如JSON),以及实现心跳机制来保持连接活跃。
WebSocket在许多场景下非常有用,比如:
实时聊天应用
在线游戏
实时数据更新(如股票价格)
协作工具
希望这个详细介绍和例子能帮助你理解C#中的WebSocket实现。如果你有任何疑问,欢迎继续提问。
上一篇:C#经验总结