2024年3月

前言

我是一个完全没用过python的人,所以,想写机器学习,就得从语法入手。

首先上W3cSchool去学习基础语法。

基础语法都差不多,重点看一下函数,模块,面向对象。

函数的写法稍有不同,格式上类似yml的写法;模块会介绍import的相关信息;面向对象会介绍类的相关信息。

参考网站:

https://www.w3cschool.cn/python3/
https://www.w3cschool.cn/python3/python3-eam72ylh.html

理论上,2~3个小时就能学完。

K-means机器学习

我这里使用VSCode进行开发,随便打开一个文件夹,然后创建一个KmeansTest.py的文件,然后点运行(右上角的三角),然后系统会提示安装python。

image

因为我电脑是Window11,所以会弹窗提示我安装python3的包,点击安装即可;如果不是window11,就自己下个python包,配置一下环境变量,这个过程不复杂。

然后,因为我是完全没有python经验的,所以我也不知道要安装什么插件,所以我就打开扩展窗口,输入python搜索,随便按几个最上面的插件。

image

然后,先在终端里执行下面代码:

 pip install scikit-learn
 pip install matplotlib

scikit-learn是做机器学习的,matplotlib是一个绘图的库。

然后我们定义个数组做为学习的源数据.

X1 = [
 [0.0,0.0],
 [0.0,0.3],
 [0.3,0.0],
 [9.0,0.0],
 [9.0,0.6],
 [9.6,0.0]]

然后编写代码:

from sklearn.cluster import KMeans
# 按F2可以重命名
xList = [
 [0.0,0.0],
 [0.0,0.3],
 [0.3,0.0],
 [9.0,0.0],
 [9.0,0.6],
 [9.6,0.0]]
n_clusters = 2
cluster = KMeans(n_clusters=n_clusters, random_state=0).fit(xList)

xLable = cluster.labels_ 
# xlable 是上面那个集合,每个元素的所属分组
print ("xLable",xLable)

xListGroup1 =[]
xListGroup2 =[]
# 使用range时,循环的是索引,python里叫序列
for index in range(len(xList)) :
   
    cluster = xLable[index]
    if(cluster==1):
        xListGroup1.append( xList[index])
    if(cluster==0):
        xListGroup2.append( xList[index])
    
 
print ("xListGroup1",xListGroup1)
print ("xListGroup2",xListGroup2)

调试得到结果:

如我们期待的一样,前3个数据比较接近,后三个比较接近,所以,分两组的话,就是前3个一组,后3个一组。

经验

调试时,删除终端再建一个,不然有时候会出现莫名奇妙的异常,而实际上,代码并没有错误,这个非常耽误时间。
image

使用matplotlib

使用matplotlib的make_blobs函数,生成一个大一点的数据源测试,代码如下:

from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans

xList, y = make_blobs(n_samples=500,n_features=2,centers=4,random_state=1)

n_clusters = 2
cluster = KMeans(n_clusters=n_clusters, random_state=0).fit(xList)

xLable = cluster.labels_ 
# xlable 是上面那个集合,每个元素的所属分组
print ("xLable",xLable)

xListGroup1 =[]
xListGroup2 =[]

# 使用range时,循环的是索引,python里叫序列
for index in range(len(xList)) :
    cluster = xLable[index]
    if(cluster==1):
        xListGroup1.append( xList[index])
    if(cluster==0):
        xListGroup2.append( xList[index])

print ("xListGroup1",xListGroup1)
print ("xListGroup2",xListGroup2)

结语

通过上面代码,我们简简单单的实现了一个机器学习。

如果想让这个功能跟项目沟通,那就学习一下网络编程,写一个http监听,然后接一组数据,用上面代码处理完,返回一组数据即可。

同理,上面的代码可以换成opencv的,可以换成TensorFlow的。


注:此文章为原创,任何形式的转载都请联系作者获得授权并注明出处!



若您觉得这篇文章还不错,请点击下方的【推荐】,非常感谢!

https://www.cnblogs.com/kiba/p/18085072

〇、前言

最近接触到 Go 语言相关的内容,由于之前都是用的 C# 语言,然后就萌生了对这两种语言进行多方面比较。

本文将在
Go 的
优势项目
协程
,来与
C# 的多线程
操作进行比较,看下差距有多大。

实际上 C# 中也有类似协程的操作,是通过 yield 关键字实现的,等后续再另做对比。

一、准备工作

先准备 1000 个同样内容的 txt 文件,内容是一串简单的字符串,以供程序读取。

程序实现的大体思路:

设置固定数量的协程(Go)或线程(C#),对 1000 个文本文件循环进行操作,取到的文件中的字符串加入到字符串列表中,然后记录耗时情况。

二、Go 语言实现

如下代码,基于(go version go1.21.0 windows/amd64),通过有缓冲的通道 channel,来设置允许同时运行 10 个协程,并通过互斥锁来保证多协程环境中,string 切片的操作安全:

package main

import (
	"fmt"
	"io/fs"
	"os"
	"sync"
	"time"
)

var mutex sync.Mutex // 互斥锁的声明

func main() {
	fmt.Println("begin-----------------!")
	path := "E:\\OA日常文件\\test\\gotestfile"

	files_entry, err := os.ReadDir(path) // 获取所有文件的路径
	if err != nil {
		fmt.Println("Error reading directory:", err)
		return
	}
	g := NewGoLimit(10) // max_num(最大允许并发数)设置为10
	strArray := []string{}
	var wg sync.WaitGroup
	fmt.Println("files_entry-len:", len(files_entry))
	start := time.Now()
	for _, file_entry := range files_entry {
		g.Add() // 尝试增加一个协程,若已达到最大并发数,将阻塞
		wg.Add(1)
		go func(file fs.DirEntry, g *GoLimit) {
			defer g.Done()
			defer wg.Done()
			// fmt.Println(goid.Get()) // 获取并打印 Goroutine id // 安装 goid 包:go get -u github.com/petermattis/goid
			content, err := os.ReadFile(path + "\\" + file.Name()) // 读取文件内容
			if err != nil {
				fmt.Println("Error reading file:", err)
				return
			}
			mutex.Lock()                                 // 加锁
			strArray = append(strArray, string(content)) // 将文件内容加入数组
			mutex.Unlock()                               // 解锁
			time.Sleep(time.Millisecond * 10)            // 模拟耗时操作
		}(file_entry, g)
	}
	wg.Wait()
	end := time.Now()
	fmt.Println("strArray-len:", len(strArray))
	fmt.Println("time.Sub(start):", end.Sub(start))
	fmt.Println("end-------------------!")
}

type GoLimit struct {
	ch chan int
}

func NewGoLimit(max int) *GoLimit {
	return &GoLimit{ch: make(chan int, max)}
}

func (g *GoLimit) Add() {
	g.ch <- 1
}

func (g *GoLimit) Done() {
	<-g.ch
}

三、C# 语言实现

如下代码,基于 .NET 7.0,通过信号量 SemaphoreSlim 控制同时工作的线程数量:

// 必要的引用
using System;
using System.IO;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
class Program
{
    private static SemaphoreSlim semaphore;
    static void Main(string[] args)
    {
        ConcurrentBag<string> strings = new ConcurrentBag<string>();
        int semaphorenum = 10;
        semaphore = new SemaphoreSlim(0, semaphorenum); // (初始数量,最大数量)
        string path = "E:\\OA日常文件\\test\\gotestfile";
        var files = Directory.GetFiles(path);
        semaphore.Release(semaphorenum); // 由于初始数量为 0 所以需要手动释放信号量
        Task[] tasks = new Task[1000];
        Console.WriteLine($"总信号量:{semaphorenum}");
        Console.WriteLine($"总文件数:{files.Length}");
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        for (int i = 0; i < files.Length; i++)
        {
            semaphore.Wait(); // 调用 Wait() 方法,标记等待进入信号量
            string content = File.ReadAllText(files[i]);
            var task = Task.Run(() =>
            {
                Thread.Sleep(10); // 模拟耗时操作
                try
                {
                    strings.Add(content);
                }
                finally
                {
                    semaphore.Release(); // 调用 Release() 方法,释放信号量
                }
                //Console.WriteLine(Thread.GetCurrentProcessorId());
            });
            tasks[i] = task;
        }
        Task.WaitAll(tasks);
        stopwatch.Stop();
        Console.WriteLine($"信号量为 {semaphorenum} 时的耗时:{stopwatch.ElapsedMilliseconds}");
    }
}

四、执行结果比较

下表是运行后耗时情况对比:

同时运行的协程/线程数 2 5 10 50 100 500 1000
Go 耗时(ms) 7740.52 3105.82 1546.60 308.87 168.50 144.24 126.00
C# 耗时(ms) 7865 3199 1646 1275 1291 1236 1286

注:耗时统计均运行多次取稳定值。

由上图可得初步结论:

在分配的协程/线程数量较少时,两种语言的操作效率相似,随着协程/线程数量的增加,Go 较 C# 效率会明显提升。

核心注解

1. @SpringBootApplication

主要用于开启自动配置,它也是一个组合注解,主要组合了 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan

2. @EnableAutoConfiguration

该注解组合了 @Import 注解,@Import 注解导入了 EnableAutoCofigurationImportSelector 类,它使用 SpringFactoriesLoader.loaderFactoryNames 方法把 spring-boot-autoconfigure.jar/META-INF/spring.factories 中的每一个 xxxAutoConfiguration 文件都加载到 IOC 容器,实现自动配置

3. @SpringBootConfiguration

@SpringBootConfiguration 注解继承自 @Configuration,二者功能基本一致,标注当前类是配置类

4. @ComponentScan

自动扫描并加载符合条件的组件,如 @Component、@Controller、@Service、@Repository 等或者 bean 定义,最终将这些 Bean 加载到 IOC 容器


Bean 相关

1. @Controller

应用于控制层,DispatcherServlet 会自动扫描此注解的类,将 web 请求映射到注解 @RequestMapping 的方法上

2. @Service

应用于业务逻辑层

3. @Reponsitory

应用于数据访问层(dao)

4. @Component

表示带有该注解的类是一个组件,可被 SpringBoot 扫描并注入 IOC 容器

5. @Configuration

表示带有该注解的类是一个配置类,通常与 @Bean 结合使用,@Configuration 继承了 @Component,因此也能被 SpringBoot 扫描并处理

6. @Bean

@Configuration 注解标识的类,使用 @Bean 注解一个可返回 Bean 的方法,Spring 会将这个 Bean 对象放入 IOC 容器


依赖注入相关

1. @Autowired

可作用在属性、方法和构造器,实现 Bean 的自动注入,默认根据类型注入

2. @Resource

作用同 @Autowired,默认通过名称注入

3. @Qualifier

如果容器中有多个相同类型的 bean,仅仅靠 @Autowired 不足以让 Spring 知道到底要注入哪个 bean,使用 @Qualifier 并指定名称可以帮助确认注入哪个 bean

4. @Value

用于注入基本类型和 String 类型


WEB 相关

1. @RequestMapping

映射 web 请求,可以注解在类和方法上,@GetMapping 和 @PostMapping 是 @RequestMapping 的两种特例,一个处理 get 请求,一个处理 post 请求

2. @RequestParam

获取请求参数,示例如下:

// http://localhost:8080/api/test1?name=liu
@RequestMapping("/test1")
@ResponseBody
public String test1(@RequestParam("name")String name1){
    System.out.println(name1);
    return name1;
}

3. @PathVariable

获取路径参数,示例如下:

@RequestMapping(value = "user/{username}")
public String test(@PathVariable(value="username") String username) {
	return "user"+username;
}

4. @RequestBody

通过 HttpMessageConverter 读取 Request Body 并反序列化为 Object,比如直接以 String 接收前端传过来的 json 数据

5. @ResponseBody

将返回值放在 response 体内,返回的是数据而不是页面,在异步请求返回 json 数据时使用


AOP 相关

1. @Aspect

声明一个切面

2. @PointCut

声明切点,即定义拦截规则,确定有哪些方法会被切入

3. @Before

前置通知,在原方法前执行

4. @After

后置通知,在原方法后执行

5. @Around

环绕通知,原方法执行前执行一次,原方法执行后再执行一次


其他注解

1. @Transactional

声明事务

2. @ControllerAdvice

作用在类上,继承了 @Component,因此也能被 SpringBoot 扫描并处理,提供对 Controller 类的拦截功能,配合 @ExceptionHandler、@InitBinder、@ModelAttribute 等注解可实现全局异常处理,全局参数绑定,请求参数预处理等功能

3. @Async

作用在方法,表示这是一个异步方法

4. @EnableAsync

注解在配置类,开启异步任务支持

5. @Scheduled

注解在方法,声明该方法是计划任务

6. @EnableScheduling

注解在配置类,开启对计划任务的支持

在 WPF 应用程序中,拖放操作是实现用户交互的重要组成部分。通过拖放操作,用户可以轻松地将数据从一个位置移动到另一个位置,或者将控件从一个容器移动到另一个容器。然而,WPF 中默认的拖放操作可能并不是那么好用。为了解决这个问题,我们可以自定义一个 Panel 来实现更简单的拖拽操作。

自定义 Panel 的优点有很多。首先,我们可以根据自己的需求来设计 Panel 的外观和行为。其次,我们可以使用代码来控制拖放操作的细节,比如拖放的开始和结束位置、拖放过程中控件的显示方式等等。最后,我们可以将自定义 Panel 作为一个控件,方便地应用到不同的应用程序中。

在本教程中,我们将一步一步地创建一个自定义 Panel 来实现更简单的拖拽操作。我们将学习如何定义 Panel 的布局、如何处理拖放事件,以及如何将自定义 Panel 应用到不同的应用程序中。按照本教程的步骤操作,您将能够创建一个功能强大且易于使用的自定义 Panel,从而使您的 WPF 应用程序更加友好和易用。

1.定义一个继承自Panel的类。

public classDragStackPanel : Panel
{
/// <summary> ///获取或设置方向/// </summary> publicOrientation Orientation
{
get { return(Orientation)GetValue(OrientationProperty); }set{ SetValue(OrientationProperty, value); }
}
public static readonly DependencyProperty OrientationProperty =DependencyProperty.Register("Orientation", typeof(Orientation), typeof(DragStackPanel), newPropertyMetadata(Orientation.Vertical));
}

2.重写Panel类的MeasureOverride方法测量控件Size。

public classDragStackPanel : Panel
{
protected overrideSize MeasureOverride(Size availableSize)
{
var panelDesiredSize = newSize();foreach (UIElement child inInternalChildren)
{
child.Measure(availableSize);
if (this.Orientation ==Orientation.Horizontal)
{
panelDesiredSize.Width
+=child.DesiredSize.Width;
panelDesiredSize.Height
= double.IsInfinity(availableSize.Height) ?child.DesiredSize.Height : availableSize.Height;
}
else{
panelDesiredSize.Width
= double.IsInfinity(availableSize.Width) ?child.DesiredSize.Width : availableSize.Width;
panelDesiredSize.Height
+=child.DesiredSize.Height;
}
}
returnpanelDesiredSize;
}
}

3.重写Panel类的ArrangeOverride方法排列控件位置。

public classDragStackPanel : Panel
{
protected overrideSize ArrangeOverride(Size finalSize)
{
double x = 0, y = 0;foreach (FrameworkElement child inInternalChildren)
{
//坐标 var position = newPoint(x, y);//宽度 var width =child.DesiredSize.Width;//高度 var height =child.DesiredSize.Height;//通过排列方向计算宽度和高度 if (this.Orientation ==Orientation.Vertical)
{
width
=finalSize.Width;
}
else{
height
=finalSize.Height;
}
//尺寸 var size = newSize(width, height);//排列位置及尺寸 child.Arrange(newRect(position, size));//计算位置 if (this.Orientation ==Orientation.Horizontal)
{
x
+=child.DesiredSize.Width;
}
else{
y
+=child.DesiredSize.Height;
}
}
returnfinalSize;
}
}

查看运行效果

<UniformGridRows="2">
    <local:DragStackPanelOrientation="Horizontal">
        <Button>test1</Button>
        <Button>test2</Button>
    </local:DragStackPanel>
    <local:DragStackPanelOrientation="Vertical">
        <Button>test3</Button>
        <Button>test4</Button>
    </local:DragStackPanel>
</UniformGrid>

4.重写PreviewMouseLeftButtonDown方法。

该方法在按下鼠标左键时触发,我们需要在该方法中获取第一次按下鼠标的坐标,并且通过命中测试找到我们要拖拽的控件,最后还要在装饰层中添加一个元素,该元素的背景用原控件的外观来填充(VisualBrush),这样就可以覆盖原来的控件,以便在拖拽控件时能跨越控件的边界。以下为参考代码:

public classDragStackPanel : Panel
{
privateFrameworkElement draggingElement;privatePoint mouseRelativePosition;private intdraggingElementzIndex;protected override voidOnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
{
//获取鼠标相对于Panel的坐标 var mousePosition = e.GetPosition(this);//通过命中测试获取当前鼠标位置下的元素 var hitTestResult = this.InputHitTest(mousePosition) asFrameworkElement;//通过命中测试结果找到当前拖拽的控件子项 draggingElement =FindChild(hitTestResult);if (draggingElement != null && this.InternalChildren.Contains(draggingElement))
{
//记录鼠标相对位置,以供后续使用 mouseRelativePosition =e.GetPosition(draggingElement);//暂存ZIndex draggingElementzIndex =Panel.GetZIndex(draggingElement);//将ZIndex置顶 Panel.SetZIndex(draggingElement, this.InternalChildren.Count);//添加遮罩,防止拖拽时覆盖 AddOverlay(draggingElement);

e.Handled
= true;
}
base.OnPreviewMouseLeftButtonDown(e);
}
}

5.重写PreviewMouseMove方法。

该方法在鼠标移动时触发,我们需要在鼠标被按下移动时,根据当前的坐标与第一次按下的坐标实时计算出被拖拽元素的偏移量,这样该元素就能跟随鼠标移动,实现拖拽效果。以下为参考代码:

public classDragStackPanel : Panel
{
privateFrameworkElement draggingElement;privatePoint mouseRelativePosition;private intdraggingElementzIndex;protected override voidOnPreviewMouseMove(MouseEventArgs e)
{
var mousePosition = e.GetPosition(this);if (e.LeftButton == MouseButtonState.Pressed && draggingElement != null)
{
//当前拖拽控件置为不可鼠标命中,以供命中下一层的换位控件 draggingElement.IsHitTestVisible = false;//判断当前拖拽的控件是否为顶层控件 if (Panel.GetZIndex(draggingElement) == this.InternalChildren.Count)
{
//计算出当前拖拽控件相对于this的位置(控件左上角) var targetPosition = new Point(mousePosition.X - mouseRelativePosition.X - draggingElement.Margin.Left, mousePosition.Y - mouseRelativePosition.Y -draggingElement.Margin.Top);//获取当前拖拽控件在this中的原始位置 var draggingElementOriginalPosition =GetDraggingElementOriginalPosition(draggingElement);//计算拖拽控件移动时的偏移量 var offset = new Point(targetPosition.X - draggingElementOriginalPosition.X, targetPosition.Y -draggingElementOriginalPosition.Y);//应用位移 draggingElement.RenderTransform = newTranslateTransform(offset.X, offset.Y);
}

e.Handled
= true;
}
base.OnPreviewMouseMove(e);
}
}

6.重写PreviewMouseLeftButtonUp方法。

该方法在鼠标左健抬起时触发,我们需要在该方法中将一些参数重置。

public classDragStackPanel : Panel
{
privateFrameworkElement draggingElement;privatePoint mouseRelativePosition;private intdraggingElementzIndex;protected override voidOnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
{
mouseRelativePosition
= default;
RemoveOverlay(draggingElement);
Panel.SetZIndex(draggingElement, draggingElementzIndex);
draggingElement.IsHitTestVisible
= true;
draggingElement.RenderTransform
= null;
draggingElement
= null;
e.Handled
= true;base.OnPreviewMouseLeftButtonUp(e);
}
}

以下为运行效果:

7.处理控件的拖拽换位。

拖拽换位的思路就是将当前正在拖拽的元素放置到新的Index中,并把该Index后面的所有元素整体后移一位。该功能在PreviewMouseMove方法中实现。

public classDragStackPanel : Panel
{
privateFrameworkElement draggingElement;privateFrameworkElement hitElement;privatePoint mouseRelativePosition;private intdraggingElementzIndex;protected override voidOnPreviewMouseMove(MouseButtonEventArgs e)
{
...
//命中当前拖拽控件的下一层控件 var hitTestResult = this.InputHitTest(mousePosition) asFrameworkElement;//查找被命中的下一层换位控件 hitElement =FindChild(hitTestResult);//判断是否有效 if (hitElement != null && this.InternalChildren.Contains(hitElement))
{
//应用换位 MoveChild(draggingElement, hitElement);
}
}
private voidMoveChild(FrameworkElement element1, FrameworkElement element2)
{
var index1 = this.InternalChildren.IndexOf(element1);var index2 = this.InternalChildren.IndexOf(element2);if (index1 >= 0 && index2 >= 0)
{
this.InternalChildren.RemoveAt(index1);this.InternalChildren.Insert(index2, element1);
}
}
}

在ArrangeOverride方法中处理重新排列时当前拖拽元素的坐标。

public classDragStackPanel : Panel
{
privateFrameworkElement draggingElement;privateFrameworkElement hitElement;privatePoint mouseRelativePosition;private intdraggingElementzIndex;protected overrideSize ArrangeOverride(Size finalSize)
{
double x = 0, y = 0;foreach (FrameworkElement child inInternalChildren)
{
...
//获取当前正在拖拽元素的位置坐标 var dragElementPosition =GetDraggingElementMovingPosition(child);if (dragElementPosition != default)
{
//处理拖拽元素坐标 var offset = new Point(dragElementPosition.X - position.X, dragElementPosition.Y -position.Y);
child.RenderTransform
= newTranslateTransform(offset.X, offset.Y);
SetDraggingElementMovingPosition(child, dragElementPosition);
}

...
}
returnfinalSize;
}
}

运行效果

8.处理跨Panel拖拽。

到目前为止已经实现了本Panel内的控件随意拖拽换位,处理从A控件拖到B控件也类似,这里需要用到一个静态变量来保存正在拖拽的控件,当B控件检测到鼠标进入时,只需要在A控件移除正在拖拽的控件,在B控件添加正在拖拽的控件就可以实现了。以下为核心代码:

public classDragStackPanel : Panel
{
//通过拖拽传递到下一个Panel的控件 private staticFrameworkElement draggingTransferElement;private void Control_MouseEnter(objectsender, MouseEventArgs e)
{
panel.Children.Remove(draggingTransferElement);
panel.DraggingElement
= null;

Panel.SetZIndex(draggingTransferElement,
this.InternalChildren.Count + 1);this.Children.Add(draggingTransferElement);this.AddOverlay(draggingTransferElement);
}
}

以下为运行效果:

9.在ListBox、ListView、DataGrid等ItemsControl中使用拖拽功能。

所有继承自ItemsControl的控件,都有一个ItemsPanel属性,该属性可以指定一个Panel类型的控件来对ItemsControl进行排列。理论上只要将ItemsControl.ItemsPanel设置为我们自己开发的Panel控件就可以实现排列及拖拽功能,但是这里直接使用的话并不会有效果。原因就是我们并没有对数据绑定的情况下做处理。它的处理逻辑也与上面的类似,首先找到ItemsControl控件,通过对ItemsSource进行操作就可以实现排列功能,由于代码大同小异这里就不再赘述。以下为ListBox控件拖拽的案例效果。

<ListBoxItemsSource="{Binding Items}">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <DragStackPanelAllowCrossBorderDrag="True"CanDragAndSort="True"IsItemsHost="True"/>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlockText="{Binding Property1}" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

10.添加动画效果。

至此基本功能已经开发完成了,下面我们为它添加上动画效果,让它更具有观赏性。动画的核心思想就是记录每个元素旧位置的坐标,当元素移动到新位置时启动一个动画,从旧坐标过渡到新坐标,由于代码太过基础,这里就不展示了,直接上效果。

<DragStackPanelAllowCrossBorderDrag="True"CanDragAndSort="True"IsItemsHost="True">
    <DragStackPanel.ChildMoveBehavior>
        <ChildMoveBehaviorDuration="0:0:0.5">
            <ChildMoveBehavior.EaseX>
                <QuinticEaseEasingMode="EaseOut" />
            </ChildMoveBehavior.EaseX>
            <ChildMoveBehavior.EaseY>
                <QuinticEaseEasingMode="EaseOut" />
            </ChildMoveBehavior.EaseY>
        </ChildMoveBehavior>
    </DragStackPanel.ChildMoveBehavior>
</DragStackPanel>

最近尝试了一下在Linux服务器上部署VSCode,也就是code-server,然后在windows电脑上通过浏览器访问打开在线编辑器编写代码,以下记录一下部署过程。

1、在linux服务器上安装code-server

a、进入到home目录下

b、创建vscode目录

c、进入vscode目录

d、使用wget命令下载code-server压缩包,我这里安装的是4.9.1版本

e、使用tar命令解压压缩包

cd /home
mkdir vscode
cd vscode
wget https:
//github.com/coder/code-server/releases/download/v4.9.1/code-server-4.9.1-linux-amd64.tar.gz tar -xvf code-server-4.9.1-linux-amd64.tar.gz

2、解压完成后,进入bin目录,执行以下命令

a、进入code-server-4.9.1-linux-amd64/bin目录

b、执行 ./code-server 命令

cd code-server-4.9.1-linux-amd64/bin
.
/code-server

c、第一次执行完 ./code-server 命令后,会在用户目录/.config/code-server下生成一个 config.yaml的配置文件,如图:

d、先使用Ctrl+C组合键退出code-server服务,使用 vi /root/.config/code-server/config.yaml 命令打开配置config.yaml文件,根据需要修改ip、端口和登录密码:

e、由于我这里是配置的8081端口,所以需要服务器放开8081端口,可以在自己的阿里云或者腾旭云服务控制后台的安全组添加8081端口,并设置策略为允许,如下图:

f、进入/home/vscode/code-server-4.9.1-linux-amd64/bin 目录下,执行 ./code-server 命令,启动code-server服务

g、此时在自己的window电脑上就可以通过:服务器ip:8081 来访问web版的vscode了,效果如下图:

第一次访问需要输入之前在config.yaml配置文件里设置的登录密码(123456),就可以愉快的在浏览器下写代码了

3、设置code-server服务保持后台在线

但是到这里还有一个问题,当前这种运行是在前台运行的,不是在后台运行的,如果运行这个code-server的话,就需要保持code-server一直在前台运行,我们的服务器就不能做其他操作了,这时我们需要使用systemctl管理来运行code-server,把code-server变成一个系统服务,可以在后台运行。操作如下:

a、使用cd命令进入 /etc/systemd/system/ 目录下

cd /etc/systemd/system/

b、使用touch命令新建一个code-server.service文件

touch code-server.service

c、使用vim编辑code-server.service为如下内容

[Unit]
Description
=code-server
After
=network.target

[Service]
Type
=exec
ExecStart
=/home/vscode/code-server-4.9.1-linux-amd64/bin/code-server
Restart
=always
User
=root

[Install]
WantedBy
=default.target

ExecStart是code-server指令所在的地址,咱们的刚刚运行code-server的时输入的linux指令
code-server
后所执行的就是/home/vscode/code-server-4.9.1-linux-amd64/bin/code-server这个可执行文件

以后就可以以下命令启动、重启、停止或卸载code-server服务了:

启动code-server:

sudo systemctl start code-server

重启code-server:

sudo systemctl status code-server

停止code-server:

sudo systemctl stop code-server

卸载code-server(卸载之前先停止code-server)

rm -rf /home/vscoderm-rf ~/.local/share/code-server
rm
-rf ~/.config/code-server
rm -rf /etc/systemd/system/code-server.service