C# wpf 实现底部嵌入HwndHost

WPF Hwnd窗口互操作系列

第一章 嵌入Hwnd窗口
第二章 嵌入WinForm控件
第三章 嵌入WPF控件
第四章 底部嵌入HwndHost(本章)


文章目录


前言

前面三章内容是笔者基于本章内容研发过程中的附带产物,但是意外的发现第三章可嵌入wpf控件后,本章的意义就变得不那么大了。本章讲述如何从底部嵌入hwnd窗口,以此来做到嵌入窗口不覆盖wpf控件的效果,这种实现思路参考了flutter的一个插件flutter_native_view,其内部实现就用了这种方式。对于wpf实现会复杂一些,因为提供自绘没有BlendMode之类的东西,无法直接消除底部画面,能够使用的方式是Clip,这种方式限制比较多。不过最终还是实现了功能。


一、如何实现?

1、底部创建窗口

(1)、创建透明窗口

csharp 复制代码
 _backgroundWindow = new Window() { WindowStyle = WindowStyle.None, ResizeMode = ResizeMode.NoResize, Focusable = false, Width = 0, Height = 0, ShowInTaskbar = false, ShowActivated = false, Background = Brushes.Transparent, Content = new Grid(), AllowsTransparency = true, };
 _backgroundHandle = (new WindowInteropHelper(_backgroundWindow)).EnsureHandle();

(2)、监控顶部窗口事件

添加hook监控上层窗口的事件

csharp 复制代码
HwndSource.FromHwnd(_forgroundHandle).AddHook(new HwndSourceHook(WndProc));

(3)、窗口保持在底部

监控相关事件保证窗口始终贴在下面。

csharp 复制代码
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
    switch (msg)
    {
        case WM_ACTIVATE:
        case WM_MOVE:
        case WM_SIZE:
            RECT rect;
            GetWindowRect(_forgroundHandle, out rect);
            SetWindowPos(_backgroundHandle, _forgroundHandle, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOACTIVATE);
            break;
    }
    return IntPtr.Zero;
}

2、HwndHost加入底部窗口

需要嵌入的hwnd窗口,直接嵌入到底部窗口。下列代码中Content是HwndHost控件,在xaml中设置。在cs代码中直接将其添加到底部窗口中。

csharp 复制代码
[ContentProperty("Content")]
public class HwndHostBottomEmbbeder : FrameworkElement
{
    public HwndHost Content
    {
        get { return (HwndHost)GetValue(HostProperty); }
        set { SetValue(HostProperty, value); }
    }
}
csharp 复制代码
var grid = _backgroundWindow.Content as Grid;
grid.Children.Add(Content);

3、Clip穿透顶部窗口

和其他嵌入方式一样,需要先继承HwndHost。clip原理参考《C# wpf利用Clip属性实现截屏框》

csharp 复制代码
//顶部窗口句柄
IntPtr _forgroundHandle = IntPtr.Zero;
//底部窗口句柄
IntPtr _backgroundHandle = IntPtr.Zero;
//底部窗口句柄
Window _backgroundWindow = null;
//顶部窗口
Window _foregroundWindow = null;
//顶部窗口的Content
FrameworkElement? _foreroundContent;
//嵌入窗口的区域
RectangleGeometry? _clipRect;
csharp 复制代码
private void NativeHost_LayoutUpdated(object? sender, EventArgs e)
{
    if (_foreroundContent == null) _foreroundContent = _foregroundWindow.Content as FrameworkElement;
    //计算控件在底部窗口的位置
    var pos = TranslatePoint(new Point(0, 0), _foregroundWindow);
    var dp = pos - Content.TranslatePoint(new Point(0, 0), _backgroundWindow);
    Content.Margin = new Thickness(Content.Margin.Left + dp.X, Content.Margin.Top + dp.Y, Content.Margin.Right - dp.X, Content.Margin.Bottom - dp.Y);
    Content.SetSize(ActualWidth, ActualHeight);
    //创建clip
    var gg = _foreroundContent.Clip as GeometryGroup;
    if (gg == null)
    {
        _foreroundContent.Clip = gg = new GeometryGroup();
        gg.FillRule = FillRule.EvenOdd;
        var rg = new RectangleGeometry();
        rg.Rect = new Rect(0, 0, _foreroundContent.ActualWidth, _foreroundContent.ActualHeight);
        gg.Children.Add(rg);
        gg.Children.Add(new CombinedGeometry() { Geometry1 = new RectangleGeometry(), Geometry2 = new GeometryGroup() { FillRule = FillRule.Nonzero } });
    }
    //底下的rect必须保持和容器大小一致
    var backRg = gg.Children.First() as RectangleGeometry;
    //上层形状即为穿透区域,CombinedGeometry类型用于支持任意个区域穿透
    var foreRg = (gg.Children[1] as CombinedGeometry).Geometry2 as GeometryGroup;
    if (_clipRect == null)
    {
        _clipRect = new RectangleGeometry();
        //添加当前控件的穿透区域
        foreRg.Children.Add(_clipRect);
    }
    var newRect = new Rect(0, 0, _foreroundContent.ActualWidth, _foreroundContent.ActualHeight);
    //判断窗口大小是否改变
    if (backRg.Rect != newRect)
    {
        backRg.Rect = newRect;
    }
    pos = TranslatePoint(new Point(0, 0), _foreroundContent);
    newRect = new Rect(pos.X, pos.Y, ActualWidth, ActualHeight);
    //判断控件区域是否变化
    if (_clipRect.Rect != newRect)
    {
        _clipRect.Rect = newRect;
    }
}

4、WPF控件放在装饰层

由于clip会截其范围内的所有控件,所有有clip的控件上面放置其他控件是看不到的,但是clip不会截取装饰层的控件,所有我们可将控件放到装饰层。

添加一个装饰器属性,在xaml中使用

csharp 复制代码
public class HwndHostBottomEmbbeder : FrameworkElement
 {
     public UIElement Adorner
     {
         get { return (UIElement)GetValue(ContentProperty); }
         set { SetValue(ContentProperty, value); }
     }
}  

定义一个装饰器对象

csharp 复制代码
class NormalAdorner : Adorner
{
    UIElement _child;
    /// <summary>
    /// 构造方法
    /// </summary>
    /// <param name="adornedElement">被添加装饰器的元素</param>
    /// <param name="child">放到装饰器中的元素</param>
    public NormalAdorner(UIElement adornedElement, UIElement child) : base(adornedElement)
    {
        _child = child;
        AddVisualChild(child);
    }
    public UIElement Child => _child;
    protected override Visual GetVisualChild(int index) => _child;
    protected override int VisualChildrenCount => 1;
    protected override System.Windows.Size ArrangeOverride(Size finalSize)
    {
        _child.Arrange(new Rect(new Point(0, 0), finalSize));
        return finalSize;
    }
}

在初始化代码中将xaml中设置的Adorner属性的控件加入到装饰层。要注意添加一个容器用于获取鼠标事件,将事件透传到原本的控件(clip后无法响应鼠标事件)。

csharp 复制代码
var layer = AdornerLayer.GetAdornerLayer(c);
if (layer == null)
    throw new Exception("获取控件装饰层失败,控件可能没有装饰层!");                       
var grid = new Grid();
grid.Background = Brushes.Transparent;
grid.DataContext = c;
grid.SetBinding(Grid.VisibilityProperty, new Binding("Visibility") { Mode = BindingMode.OneWay });
//事件透传
c.MouseByPassFrom(grid);
if (adronerContent != null)
{
    grid.Children.Add(adronerContent);
}
layer.Add(new NormalAdorner((UIElement)c, grid));

二、完整代码

1、对象定义

csharp 复制代码
 /************************************************************************
   * @Project:  	HwndHostBottomEmbbeder
   * @Decription:  底部Hwnd嵌入器,是一个控件,可包含HwndHost控件将其变成底部嵌入,wpf控件显示在装饰层上。
   *               当前版本使用限制:
   *               1、窗口需要设置WindowChrome,
   *               2、会占用Window.Content的Clip属性,
   *               3、不能点击穿透到Hwnd窗口。
   *               4、不支持半透明或全透明背景色窗口,因为嵌入hwnd窗口拖动会有残影。
   *               当前版本适用于显示视频之类的不需要交互的hwnd控件。
   * @Verision:  	v1.0.0
   * @Author:  	Xin Nie
   * @Create:  	2024/03/29 17:33:00
   * @LastUpdate:  2024/03/29 17:33:00
   ************************************************************************
   * Copyright @ 2024. All rights reserved.
   ************************************************************************/
[ContentProperty("Content")]
public class HwndHostBottomEmbbeder : FrameworkElement
{
    /// <summary>
    /// HwndHost控件
    /// </summary>
    public HwndHost Content { get; set; }
    /// <summary>
    /// 装饰层控件
    /// </summary>
    public UIElement Adorner { get; set; }
}

2、完整代码

之后上传


三、使用示例

1、嵌入Winform控件

MainWindow.xaml

xml 复制代码
<Window x:Class="WpfHwndElement.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfHwndElement"
        xmlns:ac="clr-namespace:AC"
        mc:Ignorable="d"
        Background="Transparent"
        WindowStyle="None"   
        ResizeMode="NoResize"
        Title="MainWindow" Height="360" Width="640"       
        xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"  
        >
    <WindowChrome.WindowChrome>
        <WindowChrome GlassFrameThickness="-1"   CaptionHeight="0"   />
    </WindowChrome.WindowChrome >
    <Grid  Margin="0" Background="#ffffffff"  >
        <local:HwndHostBottomEmbbeder   Height="200" Width="200" >
            <!--将WindowsFormsHost转换到底部-->
            <WindowsFormsHost>
                <wf:TextBox  Text="WinForm Text Box"    BackColor="255,	77,78,141" />
            </WindowsFormsHost>
            <!--在装饰层放wpf控件-->
            <local:HwndHostBottomEmbbeder.Adorner>
                <ToggleButton Margin="5"  Width="25" Height="30" Cursor="Hand"  >
                    <ToggleButton.Template>
                        <ControlTemplate TargetType="ToggleButton">
                            <Grid Background="Transparent">
                                <Polygon x:Name="pol" Points="0,0 25,15 0,30" Fill="Gray" Visibility="Visible"></Polygon>
                                <Rectangle  x:Name="rec1" HorizontalAlignment="Left" Width="8" Fill="Gray" Visibility="Hidden"></Rectangle>
                                <Rectangle  x:Name="rec2" HorizontalAlignment="Right"   Width="8" Fill="Gray" Visibility="Hidden"></Rectangle>
                            </Grid>
                        </ControlTemplate>
                    </ToggleButton.Template>
                </ToggleButton>
            </local:HwndHostBottomEmbbeder.Adorner>
        </local:HwndHostBottomEmbbeder>
    </Grid>
</Window>

效果预览

2、显示视频

示例代码依赖了《播放器》

MainWindow.xaml

xml 复制代码
<Window x:Class="WpfHwndElement.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfHwndElement"
        xmlns:ac="clr-namespace:AC"
        mc:Ignorable="d"
        Background="Transparent"
        WindowStyle="None"   
        ResizeMode="NoResize"
        Title="MainWindow" Height="360" Width="640"    
        xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"  
        >
    <WindowChrome.WindowChrome>
        <WindowChrome GlassFrameThickness="-1"   CaptionHeight="0"   />
    </WindowChrome.WindowChrome >
    <Grid  Margin="0" Background="#ffffffff"  >
        <local:HwndHostBottomEmbbeder   Height="200" Width="200" >
            <!--将WindowsFormsHost转换到底部-->
            <WindowsFormsHost>
                <wf:TextBox  x:Name="tb_video" Text="WinForm Text Box"    BackColor="255,77,78,141" />
            </WindowsFormsHost>
            <!--在装饰层放wpf控件-->
            <local:HwndHostBottomEmbbeder.Adorner>
                <Border Width="150" Height="150" >
                    <ToggleButton Margin="5"  Width="25" Height="30" Cursor="Hand"   Checked="ToggleButton_Checked" Unchecked="ToggleButton_Unchecked">
                        <ToggleButton.Template>
                            <ControlTemplate TargetType="ToggleButton">
                                <Grid Background="Transparent">
                                    <Polygon x:Name="pol" Points="0,0 25,15 0,30" Fill="Gray" Visibility="Visible"></Polygon>
                                    <Rectangle  x:Name="rec1" HorizontalAlignment="Left" Width="8" Fill="Gray" Visibility="Hidden"></Rectangle>
                                    <Rectangle  x:Name="rec2" HorizontalAlignment="Right"   Width="8" Fill="Gray" Visibility="Hidden"></Rectangle>
                                </Grid>
                                <ControlTemplate.Triggers>
                                    <Trigger Property="IsChecked" Value="True">
                                        <Setter TargetName="pol" Property="Visibility" Value="Hidden"></Setter>
                                        <Setter TargetName="rec1" Property="Visibility" Value="Visible"></Setter>
                                        <Setter TargetName="rec2" Property="Visibility" Value="Visible"></Setter>
                                    </Trigger>
                                </ControlTemplate.Triggers>
                            </ControlTemplate>
                        </ToggleButton.Template>
                    </ToggleButton>
                </Border>
            </local:HwndHostBottomEmbbeder.Adorner>
        </local:HwndHostBottomEmbbeder>
    </Grid>
</Window>

MainWindow.xaml.cs

csharp 复制代码
using AC;
using System.Windows;

namespace WpfHwndElement
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        Play _play;
        public MainWindow()
        {
            InitializeComponent();
        }
   
        private void ToggleButton_Checked(object sender, RoutedEventArgs e)
        {
            if (_play == null)
            {
                _play = new Play();
                _play.IsLoop = true;
                //获取tb_video的句柄,通过句柄渲染
                _play.Window = tb_video.Handle;
                _play.HardwareAccelerateType = HardwareAccelerateType.Dxva2;
                //开始播放,播放器会在传入的句柄窗口中渲染视频。
                _play.Start(@"D:\测试\Sony_4K_Camp_Main.mp4");      
            }
            else
            {
         
                _play.IsPause = false;
            }
      
        }
        private void ToggleButton_Unchecked(object sender, RoutedEventArgs e)
        {
            _play.IsPause = true;
        }
   }
}

效果预览

3、圆角矩形视频

示例代码依赖了《播放器》

MainWindow.xaml

xml 复制代码
<Window x:Class="WpfHwndElement.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfHwndElement"
        xmlns:ac="clr-namespace:AC"
        mc:Ignorable="d"
        Background="Transparent"
        WindowStyle="None"   
        ResizeMode="NoResize"
        Title="MainWindow" Height="360" Width="640"   
        ac:Resize.IsResizeable="True"
        ac:Resize.IsWindowDragSmooth="True"
        ac:Move.IsDragMoveable="True"      
        xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"  
        >
    <WindowChrome.WindowChrome>
        <WindowChrome GlassFrameThickness="-1"   CaptionHeight="0"   />
    </WindowChrome.WindowChrome >
    <Grid  Margin="0" Background="#ffffffff"  >
        <local:HwndHostBottomEmbbeder   Height="200" Width="200"   >
            <!--裁剪圆角矩形-->
            <local:HwndHostBottomEmbbeder.Clip>
                <RectangleGeometry Rect="0 0 200 200" RadiusX="10" RadiusY="10"></RectangleGeometry>
            </local:HwndHostBottomEmbbeder.Clip>
            <!--将WindowsFormsHost转换到底部-->
            <WindowsFormsHost>
                <wf:TextBox  x:Name="tb_video" Text="WinForm Text Box"  BorderStyle="None"  BackColor="255,77,78,141" />
            </WindowsFormsHost>
            <!--在装饰层放wpf控件-->
            <local:HwndHostBottomEmbbeder.Adorner>
                <Border Width="150" Height="150" >
                    <ToggleButton Margin="5"  Width="25" Height="30" Cursor="Hand"   Checked="ToggleButton_Checked" Unchecked="ToggleButton_Unchecked">
                        <ToggleButton.Template>
                            <ControlTemplate TargetType="ToggleButton">
                                <Grid Background="Transparent">
                                    <Polygon x:Name="pol" Points="0,0 25,15 0,30" Fill="Gray" Visibility="Visible"></Polygon>
                                    <Rectangle  x:Name="rec1" HorizontalAlignment="Left" Width="8" Fill="Gray" Visibility="Hidden"></Rectangle>
                                    <Rectangle  x:Name="rec2" HorizontalAlignment="Right"   Width="8" Fill="Gray" Visibility="Hidden"></Rectangle>
                                </Grid>
                                <ControlTemplate.Triggers>
                                    <Trigger Property="IsChecked" Value="True">
                                        <Setter TargetName="pol" Property="Visibility" Value="Hidden"></Setter>
                                        <Setter TargetName="rec1" Property="Visibility" Value="Visible"></Setter>
                                        <Setter TargetName="rec2" Property="Visibility" Value="Visible"></Setter>
                                    </Trigger>
                                </ControlTemplate.Triggers>
                            </ControlTemplate>
                        </ToggleButton.Template>
                    </ToggleButton>
                </Border>
            </local:HwndHostBottomEmbbeder.Adorner>
        </local:HwndHostBottomEmbbeder>
    </Grid>
</Window>

MainWindow.xaml.cs

cs代码同上略。

效果预览

4、拖动位置大小

示例代码依赖了《拖动》《调整大小》还有《播放器》

MainWindow.xaml

xml 复制代码
<Window x:Class="WpfHwndElement.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfHwndElement"
        xmlns:ac="clr-namespace:AC"
        mc:Ignorable="d"
        Background="Transparent"
        WindowStyle="None"   
        ResizeMode="NoResize"
        Title="MainWindow" Height="360" Width="640"   
        ac:Resize.IsResizeable="True"
        ac:Move.IsDragMoveable="True"      
        xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"  
        >
    <WindowChrome.WindowChrome>
        <WindowChrome GlassFrameThickness="-1"   CaptionHeight="0"   />
    </WindowChrome.WindowChrome >
    <Grid  Margin="0" Background="#ffffffff"  >
        <local:HwndHostBottomEmbbeder   Height="200" Width="200"  ac:Resize.IsResizeable="True"  ac:Move.IsDragMoveable="True" >
            <!--将WindowsFormsHost转换到底部-->
            <WindowsFormsHost>
                <!--使用窗体设置全透明-->
                <wf:Form  x:Name="tb_video"  TopLevel="False" FormBorderStyle="None"  />
            </WindowsFormsHost>
            <!--在装饰层放wpf控件-->
            <local:HwndHostBottomEmbbeder.Adorner>
                <!--非播放时wpf层填充颜色,避免直接透到桌面-->
                <Border x:Name="bd_mask"  Background="RoyalBlue">
                    <ToggleButton Margin="5"  Width="25" Height="30" Cursor="Hand"   Checked="ToggleButton_Checked" Unchecked="ToggleButton_Unchecked">
                        <ToggleButton.Template>
                            <ControlTemplate TargetType="ToggleButton">
                                <Grid Background="Transparent">
                                    <Polygon x:Name="pol" Points="0,0 25,15 0,30" Fill="Gray" Visibility="Visible"></Polygon>
                                    <Rectangle  x:Name="rec1" HorizontalAlignment="Left" Width="8" Fill="Gray" Visibility="Hidden"></Rectangle>
                                    <Rectangle  x:Name="rec2" HorizontalAlignment="Right"   Width="8" Fill="Gray" Visibility="Hidden"></Rectangle>
                                </Grid>
                                <ControlTemplate.Triggers>
                                    <Trigger Property="IsChecked" Value="True">
                                        <Setter TargetName="pol" Property="Visibility" Value="Hidden"></Setter>
                                        <Setter TargetName="rec1" Property="Visibility" Value="Visible"></Setter>
                                        <Setter TargetName="rec2" Property="Visibility" Value="Visible"></Setter>
                                    </Trigger>
                                </ControlTemplate.Triggers>
                            </ControlTemplate>
                        </ToggleButton.Template>
                    </ToggleButton>
                </Border>
            </local:HwndHostBottomEmbbeder.Adorner>
        </local:HwndHostBottomEmbbeder>
    </Grid>
</Window>

MainWindow.xaml.cs

csharp 复制代码
using AC;
using System.Reflection;
using System.Windows;

namespace WpfHwndElement
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        Play _play;
        public MainWindow()
        {

            InitializeComponent();                 
        }

        private void ToggleButton_Checked(object sender, RoutedEventArgs e)
        {
            if (_play == null)
            {

                _play = new Play();
                _play.IsLoop = true;
                //获取tb_video的句柄,通过句柄渲染
                _play.Window = tb_video.Handle;
                _play.HardwareAccelerateType = HardwareAccelerateType.Dxva2;
                //开始播放,播放器会在传入的句柄窗口中渲染视频。
                _play.Start(@"D:\Sony_4K_Camp.mp4");
                //实现透明窗口,避免拖动时与视频绘制冲突,出现界面闪烁
                tb_video.GetType().GetMethod("SetStyle", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(tb_video, new object[] { ControlStyles.Opaque, true });
            }
            else
            {
         
                _play.IsPause = false;
            }
            bd_mask.Background =System.Windows.Media. Brushes.Transparent;

        }
        private void ToggleButton_Unchecked(object sender, RoutedEventArgs e)
        {
            _play.IsPause = true;
            bd_mask.Background = System.Windows.Media.Brushes.RoyalBlue;
        }
  }
}

总结

以上就是今天要讲的内容,本章的实现是有一定难度的,其灵感来源有flutter,也是刚好发现wpf的Clip属性能做到穿透,才有了实现本章的基础,但是限制也不小,当然作为初步探索的版本,以后可以继续优化。本章实现的嵌入方式虽然有限制,但还是能够用来做视频渲染的,尤其是需要做圆角边框以及拖动的场景,就能做到渲染性能和界面效果的完美结合。

相关推荐
凡人的AI工具箱10 分钟前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
chnming198726 分钟前
STL关联式容器之map
开发语言·c++
进击的六角龙27 分钟前
深入浅出:使用Python调用API实现智能天气预报
开发语言·python
檀越剑指大厂27 分钟前
【Python系列】浅析 Python 中的字典更新与应用场景
开发语言·python
湫ccc35 分钟前
Python简介以及解释器安装(保姆级教学)
开发语言·python
程序伍六七39 分钟前
day16
开发语言·c++
wkj00143 分钟前
php操作redis
开发语言·redis·php
极客代码1 小时前
【Python TensorFlow】进阶指南(续篇三)
开发语言·人工智能·python·深度学习·tensorflow
土豆湿1 小时前
拥抱极简主义前端开发:NoCss.js 引领无 CSS 编程潮流
开发语言·javascript·css
界面开发小八哥1 小时前
更高效的Java 23开发,IntelliJ IDEA助力全面升级
java·开发语言·ide·intellij-idea·开发工具