在吕毅大佬的文章中已经详细介绍了什么是AppBar: WPF 使用 AppBar 将窗口停靠在桌面上,让其他程序不占用此窗口的空间(附我封装的附加属性) - walterlv

即让窗口固定在屏幕某一边,并且保证其他窗口最大化后不会覆盖AppBar占据区域(类似于Windows任务栏)。

但是在我的环境中测试时,上面的代码出现了一些问题,例如非100%缩放显示时的坐标计算异常、多窗口同时停靠时布局错乱等。所以我重写了AppBar在WPF上的实现,效果如图:

  

一、AppBar的主要申请流程

主要流程如图:

核心代码其实在于如何计算停靠窗口的位置,要点是处理好一下几个方面:

1. 修改停靠位置时用原窗口的大小计算,被动告知需要调整位置时用即时大小计算

2. 像素单位与WPF单位之间的转换

3. 小心Windows的位置建议,并排停靠时会得到负值高宽,需要手动适配对齐方式

4. 有新的AppBar加入时,窗口会被系统强制移动到工作区(WorkArea),这点我还没能找到解决方案,只能把移动窗口的命令通过Dispatcher延迟操作

二、如何使用

1.下载我封装好的库:AppBarTest/AppBarCreator.cs at master · TwilightLemon/AppBarTest (github.com)

2.  在xaml中直接设置:

<Window ...>

<local:AppBarCreator.AppBar>
<local:AppBar x:Name="appBar" Location="Top" OnFullScreenStateChanged="AppBar_OnFullScreenStateChanged"/>
</local:AppBarCreator.AppBar> ...
</Window>

或者在后台创建:

private readonly AppBar appBar=new AppBar();

...Window_Loaded...
appBar.Location = AppBarLocation.Top;
appBar.OnFullScreenStateChanged += AppBar_OnFullScreenStateChanged;
AppBarCreator.SetAppBar(this, appBar);

3. 另外你可能注意到了,这里有一个OnFullScreenStateChanged事件:该事件由AppBarMsg注册,在有窗口进入或退出全屏时触发,参数bool为true指示进入全屏。

你需要手动在事件中设置全屏模式下的行为,例如在全屏时隐藏AppBar

    private void AppBar_OnFullScreenStateChanged(object sender, bool e)
{
Debug.WriteLine("Full Screen State: "+e);
Visibility = e ? Visibility.Collapsed : Visibility.Visible;
}

我在官方的Flag上加了一个RegisterOnly,即只注册AppBarMsg而不真的停靠窗口,可以此用来作全屏模式监听。

4. 如果你需要在每个虚拟桌面都显示AppBar(像任务栏那样),可以尝试为窗口使用SetWindowLong添加WS_EX_TOOLWINDOW标签(自行查找)

以下贴出完整的代码:

  1 using System.ComponentModel;
2 using System.Diagnostics;
3 using System.Runtime.InteropServices;
4 using System.Windows;
5 using System.Windows.Interop;
6 using System.Windows.Threading;
7
8 namespace AppBarTest;
9 public static class AppBarCreator
10 {
11 public static readonly DependencyProperty AppBarProperty =
12 DependencyProperty.RegisterAttached(
13 "AppBar",
14 typeof(AppBar),
15 typeof(AppBarCreator),
16 new PropertyMetadata(null, OnAppBarChanged));
17 private static void OnAppBarChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
18 {
19 if (d is Window window && e.NewValue is AppBar appBar)
20 {
21 appBar.AttachedWindow = window;
22 }
23 }
24 public static void SetAppBar(Window element, AppBar value)
25 {
26 if (value == null) return;
27 element.SetValue(AppBarProperty, value);
28 }
29
30 public static AppBar GetAppBar(Window element)
31 {
32 return (AppBar)element.GetValue(AppBarProperty);
33 }
34 }
35
36 public class AppBar : DependencyObject
37 {
38 /// <summary>
39 /// 附加到的窗口
40 /// </summary>
41 public Window AttachedWindow
42 {
43 get => _window;
44 set
45 {
46 if (value == null) return;
47 _window = value;
48 _window.Closing += _window_Closing;
49 _window.LocationChanged += _window_LocationChanged;
50 //获取窗口句柄hWnd
51 var handle = new WindowInteropHelper(value).Handle;
52 if (handle == IntPtr.Zero)
53 {
54 //Win32窗口未创建
55 _window.SourceInitialized += _window_SourceInitialized;
56 }
57 else
58 {
59 _hWnd = handle;
60 CheckPending();
61 }
62 }
63 }
64
65 private void _window_LocationChanged(object? sender, EventArgs e)
66 {
67 Debug.WriteLine(_window.Title+ " LocationChanged: Top: "+_window.Top+" Left: "+_window.Left);
68 }
69
70 private void _window_Closing(object? sender, CancelEventArgs e)
71 {
72 _window.Closing -= _window_Closing;
73 if (Location != AppBarLocation.None)
74 DisableAppBar();
75 }
76
77 /// <summary>
78 /// 检查是否需要应用之前的Location更改
79 /// </summary>
80 private void CheckPending()
81 {
82 //创建AppBar时提前触发的LocationChanged
83 if (_locationChangePending)
84 {
85 _locationChangePending = false;
86 LoadAppBar(Location);
87 }
88 }
89 /// <summary>
90 /// 载入AppBar
91 /// </summary>
92 /// <param name="e"></param>
93 private void LoadAppBar(AppBarLocation e,AppBarLocation? previous=null)
94 {
95
96 if (e != AppBarLocation.None)
97 {
98 if (e == AppBarLocation.RegisterOnly)
99 {
100 //仅注册AppBarMsg
101 //如果之前注册过有效的AppBar则先注销,以还原位置
102 if (previous.HasValue && previous.Value != AppBarLocation.RegisterOnly)
103 {
104 if (previous.Value != AppBarLocation.None)
105 {
106 //由生效的AppBar转为RegisterOnly,还原为普通窗口再注册空AppBar
107 DisableAppBar();
108 }
109 RegisterAppBarMsg();
110 }
111 else
112 {
113 //之前未注册过AppBar,直接注册
114 RegisterAppBarMsg();
115 }
116 }
117 else
118 {
119 if (previous.HasValue && previous.Value != AppBarLocation.None)
120 {
121 //之前为RegisterOnly才备份窗口信息
122 if(previous.Value == AppBarLocation.RegisterOnly)
123 {
124 BackupWindowInfo();
125 }
126 SetAppBarPosition(_originalSize);
127 ForceWindowStyles();
128 }
129 else
130 EnableAppBar();
131 }
132 }
133 else
134 {
135 DisableAppBar();
136 }
137 }
138 private void _window_SourceInitialized(object? sender, EventArgs e)
139 {
140 _window.SourceInitialized -= _window_SourceInitialized;
141 _hWnd = new WindowInteropHelper(_window).Handle;
142 CheckPending();
143 }
144
145 /// <summary>
146 /// 当有窗口进入或退出全屏时触发 bool参数为true时表示全屏状态
147 /// </summary>
148 public event EventHandler<bool>? OnFullScreenStateChanged;
149 /// <summary>
150 /// 期望将AppBar停靠到的位置
151 /// </summary>
152 public AppBarLocation Location
153 {
154 get { return (AppBarLocation)GetValue(LocationProperty); }
155 set { SetValue(LocationProperty, value); }
156 }
157
158 public static readonly DependencyProperty LocationProperty =
159 DependencyProperty.Register(
160 "Location",
161 typeof(AppBarLocation), typeof(AppBar),
162 new PropertyMetadata(AppBarLocation.None, OnLocationChanged));
163
164 private bool _locationChangePending = false;
165 private static void OnLocationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
166 {
167 if (DesignerProperties.GetIsInDesignMode(d))
168 return;
169 if (d is not AppBar appBar) return;
170 if (appBar.AttachedWindow == null)
171 {
172 appBar._locationChangePending = true;
173 return;
174 }
175 appBar.LoadAppBar((AppBarLocation)e.NewValue,(AppBarLocation)e.OldValue);
176 }
177
178 private int _callbackId = 0;
179 private bool _isRegistered = false;
180 private Window _window = null;
181 private IntPtr _hWnd;
182 private WindowStyle _originalStyle;
183 private Point _originalPosition;
184 private Size _originalSize = Size.Empty;
185 private ResizeMode _originalResizeMode;
186 private bool _originalTopmost;
187 public Rect? DockedSize { get; set; } = null;
188 private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam,
189 IntPtr lParam, ref bool handled)
190 {
191 if (msg == _callbackId)
192 {
193 Debug.WriteLine(_window.Title + " AppBarMsg("+_callbackId+"): " + wParam.ToInt32() + " LParam: " + lParam.ToInt32());
194 switch (wParam.ToInt32())
195 {
196 case (int)Interop.AppBarNotify.ABN_POSCHANGED:
197 Debug.WriteLine("AppBarNotify.ABN_POSCHANGED ! "+_window.Title);
198 if (Location != AppBarLocation.RegisterOnly)
199 SetAppBarPosition(Size.Empty);
200 handled = true;
201 break;
202 case (int)Interop.AppBarNotify.ABN_FULLSCREENAPP:
203 OnFullScreenStateChanged?.Invoke(this, lParam.ToInt32() == 1);
204 handled = true;
205 break;
206 }
207 }
208 return IntPtr.Zero;
209 }
210
211 public void BackupWindowInfo()
212 {
213 _callbackId = 0;
214 DockedSize = null;
215 _originalStyle = _window.WindowStyle;
216 _originalSize = new Size(_window.ActualWidth, _window.ActualHeight);
217 _originalPosition = new Point(_window.Left, _window.Top);
218 _originalResizeMode = _window.ResizeMode;
219 _originalTopmost = _window.Topmost;
220 }
221 public void RestoreWindowInfo()
222 {
223 if (_originalSize != Size.Empty)
224 {
225 _window.WindowStyle = _originalStyle;
226 _window.ResizeMode = _originalResizeMode;
227 _window.Topmost = _originalTopmost;
228 _window.Left = _originalPosition.X;
229 _window.Top = _originalPosition.Y;
230 _window.Width = _originalSize.Width;
231 _window.Height = _originalSize.Height;
232 }
233 }
234 public void ForceWindowStyles()
235 {
236 _window.WindowStyle = WindowStyle.None;
237 _window.ResizeMode = ResizeMode.NoResize;
238 _window.Topmost = true;
239 }
240
241 public void RegisterAppBarMsg()
242 {
243 var data = new Interop.APPBARDATA();
244 data.cbSize = Marshal.SizeOf(data);
245 data.hWnd = _hWnd;
246
247 _isRegistered = true;
248 _callbackId = Interop.RegisterWindowMessage(Guid.NewGuid().ToString());
249 data.uCallbackMessage = _callbackId;
250 var success = Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_NEW, ref data);
251 var source = HwndSource.FromHwnd(_hWnd);
252 Debug.WriteLineIf(source == null, "HwndSource is null!");
253 source?.AddHook(WndProc);
254 Debug.WriteLine(_window.Title+" RegisterAppBarMsg: " + _callbackId);
255 }
256 public void EnableAppBar()
257 {
258 if (!_isRegistered)
259 {
260 //备份窗口信息并设置窗口样式
261 BackupWindowInfo();
262 //注册成为AppBar窗口
263 RegisterAppBarMsg();
264 ForceWindowStyles();
265 }
266 //成为AppBar窗口之后(或已经是)只需要注册并移动窗口位置即可
267 SetAppBarPosition(_originalSize);
268 }
269 public void SetAppBarPosition(Size WindowSize)
270 {
271 var data = new Interop.APPBARDATA();
272 data.cbSize = Marshal.SizeOf(data);
273 data.hWnd = _hWnd;
274 data.uEdge = (int)Location;
275 data.uCallbackMessage = _callbackId;
276 Debug.WriteLine("\r\nWindow: "+_window.Title);
277
278 //获取WPF单位与像素的转换矩阵
279 var compositionTarget = PresentationSource.FromVisual(_window)?.CompositionTarget;
280 if (compositionTarget == null)
281 throw new Exception("居然获取不到CompositionTarget?!");
282 var toPixel = compositionTarget.TransformToDevice;
283 var toWpfUnit = compositionTarget.TransformFromDevice;
284
285 //窗口在屏幕的实际大小
286 if(WindowSize== Size.Empty)
287 WindowSize = new Size(_window.ActualWidth, _window.ActualHeight);
288 var actualSize = toPixel.Transform(new Vector(WindowSize.Width, WindowSize.Height));
289 //屏幕的真实像素
290 var workArea = toPixel.Transform(new Vector(SystemParameters.PrimaryScreenWidth, SystemParameters.PrimaryScreenHeight));
291 Debug.WriteLine("WorkArea Width: {0}, Height: {1}", workArea.X, workArea.Y);
292
293 if (Location is AppBarLocation.Left or AppBarLocation.Right)
294 {
295 data.rc.top = 0;
296 data.rc.bottom = (int)workArea.Y;
297 if (Location == AppBarLocation.Left)
298 {
299 data.rc.left = 0;
300 data.rc.right = (int)Math.Round(actualSize.X);
301 }
302 else
303 {
304 data.rc.right = (int)workArea.X;
305 data.rc.left = (int)workArea.X - (int)Math.Round(actualSize.X);
306 }
307 }
308 else
309 {
310 data.rc.left = 0;
311 data.rc.right = (int)workArea.X;
312 if (Location == AppBarLocation.Top)
313 {
314 data.rc.top = 0;
315 data.rc.bottom = (int)Math.Round(actualSize.Y);
316 }
317 else
318 {
319 data.rc.bottom = (int)workArea.Y;
320 data.rc.top = (int)workArea.Y - (int)Math.Round(actualSize.Y);
321 }
322 }
323 //以上生成的是四周都没有其他AppBar时的理想位置
324 //系统将自动调整位置以适应其他AppBar
325 Debug.WriteLine("Before QueryPos: Left: {0}, Top: {1}, Right: {2}, Bottom: {3}", data.rc.left, data.rc.top, data.rc.right, data.rc.bottom);
326 Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_QUERYPOS, ref data);
327 Debug.WriteLine("After QueryPos: Left: {0}, Top: {1}, Right: {2}, Bottom: {3}", data.rc.left, data.rc.top, data.rc.right, data.rc.bottom);
328 //自定义对齐方式,确保Height和Width不会小于0
329 if (data.rc.bottom - data.rc.top < 0)
330 {
331 if (Location == AppBarLocation.Top)
332 data.rc.bottom = data.rc.top + (int)Math.Round(actualSize.Y);//上对齐
333 else if (Location == AppBarLocation.Bottom)
334 data.rc.top = data.rc.bottom - (int)Math.Round(actualSize.Y);//下对齐
335 }
336 if(data.rc.right - data.rc.left < 0)
337 {
338 if (Location == AppBarLocation.Left)
339 data.rc.right = data.rc.left + (int)Math.Round(actualSize.X);//左对齐
340 else if (Location == AppBarLocation.Right)
341 data.rc.left = data.rc.right - (int)Math.Round(actualSize.X);//右对齐
342 }
343 //调整完毕,设置为最终位置
344 Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_SETPOS, ref data);
345 //应用到窗口
346 var location = toWpfUnit.Transform(new Point(data.rc.left, data.rc.top));
347 var dimension = toWpfUnit.Transform(new Vector(data.rc.right - data.rc.left,
348 data.rc.bottom - data.rc.top));
349 var rect = new Rect(location, new Size(dimension.X, dimension.Y));
350 DockedSize = rect;
351
352 _window.Dispatcher.Invoke(DispatcherPriority.ApplicationIdle, () =>{
353 _window.Left = rect.Left;
354 _window.Top = rect.Top;
355 _window.Width = rect.Width;
356 _window.Height = rect.Height;
357 });
358
359 Debug.WriteLine("Set {0} Left: {1} ,Top: {2}, Width: {3}, Height: {4}", _window.Title, _window.Left, _window.Top, _window.Width, _window.Height);
360 }
361 public void DisableAppBar()
362 {
363 if (_isRegistered)
364 {
365 _isRegistered = false;
366 var data = new Interop.APPBARDATA();
367 data.cbSize = Marshal.SizeOf(data);
368 data.hWnd = _hWnd;
369 data.uCallbackMessage = _callbackId;
370 Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_REMOVE, ref data);
371 _isRegistered = false;
372 RestoreWindowInfo();
373 Debug.WriteLine(_window.Title + " DisableAppBar");
374 }
375 }
376 }
377
378 public enum AppBarLocation : int
379 {
380 Left = 0,
381 Top,
382 Right,
383 Bottom,
384 None,
385 RegisterOnly=99
386 }
387
388 internal static class Interop
389 {
390 #region Structures & Flags
391 [StructLayout(LayoutKind.Sequential)]
392 internal struct RECT
393 {
394 public int left;
395 public int top;
396 public int right;
397 public int bottom;
398 }
399
400 [StructLayout(LayoutKind.Sequential)]
401 internal struct APPBARDATA
402 {
403 public int cbSize;
404 public IntPtr hWnd;
405 public int uCallbackMessage;
406 public int uEdge;
407 public RECT rc;
408 public IntPtr lParam;
409 }
410
411 internal enum AppBarMsg : int
412 {
413 ABM_NEW = 0,
414 ABM_REMOVE,
415 ABM_QUERYPOS,
416 ABM_SETPOS,
417 ABM_GETSTATE,
418 ABM_GETTASKBARPOS,
419 ABM_ACTIVATE,
420 ABM_GETAUTOHIDEBAR,
421 ABM_SETAUTOHIDEBAR,
422 ABM_WINDOWPOSCHANGED,
423 ABM_SETSTATE
424 }
425 internal enum AppBarNotify : int
426 {
427 ABN_STATECHANGE = 0,
428 ABN_POSCHANGED,
429 ABN_FULLSCREENAPP,
430 ABN_WINDOWARRANGE
431 }
432 #endregion
433
434 #region Win32 API
435 [DllImport("SHELL32", CallingConvention = CallingConvention.StdCall)]
436 internal static extern uint SHAppBarMessage(int dwMessage, ref APPBARDATA pData);
437
438 [DllImport("User32.dll", CharSet = CharSet.Auto)]
439 internal static extern int RegisterWindowMessage(string msg);
440 #endregion
441 }

三、已知问题

1.在我的github上的实例程序中,如果你将两个同进程的窗口并排叠放的话,会导致explorer和你的进程双双爆栈,windows似乎不能很好地处理这两个并排放置地窗口,一直在左右调整位置,疯狂发送ABN_POSCHANGED消息。(快去clone试试,死机了不要打我) 但是并排放置示例窗口和OneNote地Dock窗口就没有问题。

2.计算停靠窗口时,如果选择停靠位置为Bottom,则系统建议的bottom位置值会比实际的高,测试发现是任务栏窗口占据了部分空间,应该是预留给平板模式的更大图标任务栏(猜测,很不合理的设计)

自动隐藏任务栏就没有这个问题:

3. 没有实现自动隐藏AppBar,故没有处理与之相关的WM_ACTIVATE等消息,有需要的可以参考官方文档。(嘻 我懒)

参考文档:

1). SHAppBarMessage function (shellapi.h) - Win32 apps | Microsoft Learn

2). ABM_QUERYPOS message (Shellapi.h) - Win32 apps | Microsoft Learn ABM_NEW & ABM_SETPOS etc..

3). 使用应用程序桌面工具栏 - Win32 apps | Microsoft Learn

4). 判断是否有全屏程序正在运行(C#)_c# 判断程序当前窗口是否全屏如果是返回原来-CSDN博客

[打个广告] [入门AppBar的最佳实践]

看这里,如果你也需要一个高度可自定义的沉浸式顶部栏(Preview): TwilightLemon/MyToolBar: 为Surface Pro而生的顶部工具栏 支持触控和笔快捷方式 (github.com)

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名TwilightLemon和原文网址,不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

WPF使用AppBar实现窗口停靠,适配缩放、全屏响应和多窗口并列(附封装好即开即用的附加属性)的更多相关文章

  1. DirectX全屏游戏中弹出窗口(转)

    一直有人问如何在DirectX全屏游戏中弹出窗口就象金山游侠一样.我答应过要给出原码,只是一直没有时间整理,不过现在总算是弄玩了.代码不长,大致作了些注释,但愿你能看懂:)按照我的说明一步步作应该就能 ...

  2. 桌面应用(.exe)设置窗口默认最大化、全屏(electron)

    设置窗口默认最大化.全屏(electron) 一.默认最大化 win = new BrowserWindow({show: false}) win.maximize() win.show() 二.默认 ...

  3. wpf仿qq边缘自动停靠,支持多屏

    wpf完全模仿qq边缘自动隐藏功能,采用鼠标钩子获取鼠标当前状态,在通过当前鼠标的位置和点击状态来计算是否需要隐藏. 以下是实现的具体方法: 一.鼠标钩子实时获取当前鼠标的位置和点击状态 /// &l ...

  4. MFC全屏显示和多窗口动态显示的一些技巧和方法

    一.全屏 1.全屏窗口从dialogex继承,因为要处理一些东西 2.全屏代码,这样设置后尺寸不会出bug,只设置为最大值的话容易出bug //get current system resolutio ...

  5. c# 窗口API,以及全屏锁定一些tips

    this.WindowState = FormWindowState.Maximized; this.FormBorderStyle = FormBorderStyle.None; /* FormBo ...

  6. webdriver设置浏览器全屏及设置浏览器窗口为特定大小的方法

    from selenium import webdriver driver = webdriver.Chrome() #全屏 driver.maximize_window() #具体大小 driver ...

  7. QT中关于窗口全屏显示与退出全屏的实现

    近期在学习QT时遇到了很多问题这也是其中一个,个人通过在各种书籍和网络上的查阅找到了一些关于这方面的答案,希望能给大家一些帮助. 首先,在QT中对于窗口显示常用的有这么几个方法可以调用: Qt全屏显示 ...

  8. Qt全屏显示窗口、子窗口的相关函数

    Qt全屏显示函数         window.showFullScreen() Qt最大化显示函数         window.showMaximized() Qt最小化显示函数         ...

  9. QT 子窗口退出全屏

    m_pWidget代表子窗口, 子窗口显示全屏: m_pWidget->setWindowFlags(Qt::Dialog); m_pWidget->showFullScreen(); 子 ...

  10. jquery3和layui冲突导,致使用layui.layer.full弹出全屏iframe窗口时高度152px问题

    项目中使用的jquery版本是jquery-3.2.1,在使用layui弹出全屏iframe窗口时,iframe窗口顶部总是出现一个152px高的滚动窗口无法实现真正全屏,代码如下: <!DOC ...

随机推荐

  1. golang select 和外层的 for 搭配

    select语句通常与for循环搭配使用,但并不是必须的. 在某些情况下,select可能会直接放在一个独立的goroutine中,没有外层的for循环. 这通常发生在你知道只会有一次或有限次操作的情 ...

  2. .NET Core 中使用GBK GB2312编码报错的问题

    错误描述 环境 dotnet core 2.1 2.2   dotnet core 3.1 dotnet core 5.0 现象 当代码中使用 System.Text.Encoding.GetEnco ...

  3. springboot~封装依赖引用包jar还是pom,哪种更规范

    将多个第三方包封装成一个项目后,如果你的目的是让其他开发人员可以直接引用这些依赖,一般来说有两种常见的方式: 打成JAR包:将封装好的项目编译打包成JAR文件,其他开发人员可以将这个JAR文件添加到他 ...

  4. Flask-Limit详细说明:接口限流

    速率限制通常作为服务的防御措施予以实施.服务需要保护自身以免过度使用(无论是有意还是无意),从而保持服务可用性.在Flask项目开发过程中,遇到了需要对接口进行限制的需求,又不想去造轮子,这时候就需要 ...

  5. OpenCV笔记(6) Bitwise

    源码: BitwiseAnd   //dst = src1 & src2 public static void BitwiseAnd(InputArray src1, InputArray s ...

  6. MySQL学习笔记-索引

    索引 索引(index)是帮助MySQL高效获取数据的数据结构(有序).在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现 ...

  7. Semantic Kernel入门系列:通过依赖注入管理对象和插件

    前言 本章讲一下在Semantic Kernel中使用DependencyInject(依赖注入),在之前的章节我们都是通过手动创建Kernel对象来完成框架的初始化工作,今天我们用依赖注入的方式来实 ...

  8. 利用夜莺开源版对H3C无线设备监控

    编者荐语:真正搞监控的人肯定知道 SNMP 水有多深,有时我甚至腹黑猜测,这些厂商是故意的吧,,,指标不标准,格式各异,只能靠一款灵活的采集器了,本文是夜莺社区用户写的文章,转给大家参考. autho ...

  9. word文档生成视频,自动配音、背景音乐、自动字幕,另类创作工具

    简介 不同于别的视频创作工具,这个工具创作视频只需要在word文档中打字,插入图片即可.完事后就能获得一个带有配音.字幕.背景音乐.视频特效滤镜的优美作品. 这种不要门槛,没有技术难度的视频创作工具, ...

  10. springboot项目编译时,使用自定义注解类找不到符号

    springboot项目编译时,使用自定义注解类找不到符号 Java项目编译时,使用自定义注解类找不到符号Spring-boot项目编辑器:idea问题:编译时找不到符号.项目中用到了自定义注解类.编 ...