- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
国庆假期各种活动比较多,直到上班才有时间来更新文章~ 。
不过这两天我还是做了个小玩意(Clipify),起因是想给之前开发来自己用的简单视频剪辑工具 QuickCutSharp 加个功能,不过这个软件是基于 WinForms 开发的,做界面得拖拉控件,感觉繁琐又不灵活,于是索性重新做一个.
原有代码是C#,于是我就继续在这个生态里寻找开发方案,Avalonia、MAUI等都是不错的选择,前者我之前用过,做了个简单的图片管理工具,后者听说是微软新推出的跨平台开发方案,我这次也试了一下,不过单纯处理环境就比较复杂了,直接劝退.
接下来我就把目光瞄准了类似 Electron 这类套壳开发,既然要用前端技术开发软件界面,那么 C# 生态的 Blazor 就可以拿出来了,我之前也用 Blazor 开发过几个项目,感觉使用 Blazor 搭配 TailwindCSS 应该可以有不错的开发体验.
说干就干,我选择了 Blazor Hybrid 这个方向,然后宿主容器依然选择 WinForms,原因是暂时没有跨平台的需求,而且 Blazor Hybrid 目前也没有比较好的跨平台方案,虽然有 MAUI 但太重而且也不支持 Linux… 。
项目已经开源,Github: https://github.com/Deali-Axy/clipify 。
老规矩前面先放一些截图,软件的功能直接看图就清楚了.
软件主页 。
提取音频界面 。
导出视频界面 。
PS:目前只实现了部分功能 。
正如前言说到的,使用了 Blazor Hybrid 来开发,那么界面就是 Blazor 实现的,然后运行在一个 Winforms 软件的 BlazorWebView 中.
视频相关的功能是调用了 ffmpeg (实际上在没有这个软件之前,我都是手动输入命令操作的…) 。
前端方面依然是 pnpm、gulp、tailwindcss、flowbite、fortawesome 这些 。
Electron技术大家都很熟悉了,现在连QQ都用Electron重构了,在开发了这个项目之后,我也能理解这种做法,用前端技术来写界面真的爽,只要稍微牺牲一下性能,就可以获得不错的效果,而且现在电脑的性能都已经足够了,正好给web技术上桌面提供了条件.
而 Blazor 对于 C# 开发人员的优势是不需要学习各种 JavaScript 框架就可以开发交互式的 web 应用;虽然我做过不少前端项目,React也用得比较熟了,不过 Blazor Hybrid 还有一个优势是可以直接使用 C# 调用系统功能,Blazor Hybrid 一方面是运行在浏览器中,一方面又是直接在操作系统层面运行,C# 代码可以不受浏览器沙箱的限制,直接访问系统文件、设备等(虽然本项目中还是用到了Blazor与WinForms通信,不过那不是 C# 的功能限制,而是必须用到 WinForms 的功能).
创建一个基于 WinForms 的 Blazor Hybrid 项目很简单,首先是创建 .NetCore(.Net8) 的 WinForms 项目,然后添加 Microsoft.AspNetCore.Components.WebView.WindowsForms 依赖 。
接着把 BlazorWebView 组件添加到 Form 上面 。
然后开始写代码初始化 。
public partial class FormMain : Form {
public FormMain() {
InitializeComponent();
var services = new ServiceCollection();
services.AddLogging(c => {
c.AddDebug();
c.AddFilter("Microsoft.AspNetCore.Components.WebView", LogLevel.Trace);
});
services.AddAntDesign();
services.AddMediatR(cfg => { cfg.RegisterServicesFromAssemblyContaining<FormMain>(); });
services.AddWindowsFormsBlazorWebView();
#if DEBUG
services.AddBlazorWebViewDeveloperTools();
#endif
services.AddSingleton(this);
services.AddScoped<IHostingEnvironment, HostingEnvironment>();
services.AddScoped<DialogService>();
services.AddScoped<VideoService>();
blazorWebView1.HostPage = "wwwroot\\index.html";
blazorWebView1.Services = services.BuildServiceProvider();
blazorWebView1.RootComponents.Add<App>("#app");
}
}
关键的就在于最下面的三行代码,设置主页、把服务容器绑定的 Blazor 控件上,设置根组件.
然后其他的就和普通的 Blazor 项目一样.
本文限于篇幅,只能简单介绍一下.
想要进一步了解的同学可以看官网的指引文档和实例项目.
不过微软官网关于这方面的文档也不是很详细,只是浅尝辄止,很多内容要靠自己摸索.
按需添加了各种 css 和 js 引用 。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Clipify</title>
<base href="/"/>
<link href="css/app.css" rel="stylesheet"/>
<link href="css/tailwind.min.css" rel="stylesheet"/>
<link href="lib/font-awesome/css/all.min.css" rel="stylesheet">
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<link href="Clipify.Forms.styles.css" rel="stylesheet"/>
</head>
<body>
<div id="app">Loading...</div>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webview.js"></script>
<script src="lib/flowbite/flowbite.min.js"></script>
<script src="_content/AntDesign/js/ant-design-blazor.js"></script>
<script>
window.initializeFlowbite = () => {
initFlowbite();
}
</script>
</body>
</html>
这个是根组件 。
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
<AntContainer />
布局组件.
@inherits LayoutComponentBase
@inject IJSRuntime Js
<PageTitle>Clipify</PageTitle>
<button data-drawer-target="logo-sidebar" data-drawer-toggle="logo-sidebar" aria-controls="logo-sidebar" type="button" class="inline-flex items-center p-2 mt-2 ms-3 text-sm text-gray-500 rounded-lg sm:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600">
<span class="sr-only">Open sidebar</span>
<svg class="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path clip-rule="evenodd" fill-rule="evenodd" d="M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z"></path>
</svg>
</button>
<aside id="logo-sidebar" class="fixed top-0 left-0 z-40 w-64 h-screen transition-transform -translate-x-full sm:translate-x-0" aria-label="Sidebar">
<Navbar/>
</aside>
<div class="p-4 sm:ml-64">
@Body
</div>
@code {
protected override async Task OnAfterRenderAsync(bool isFirstRender) {
#if DEBUG
await Js.InvokeVoidAsync("window.initializeFlowbite");
#endif
if (isFirstRender) {
await Js.InvokeVoidAsync("window.initializeFlowbite");
}
}
}
基础功能到这里就搞定了 。
我习惯在项目里加一个 RouterMap ,这样在路由跳转的时候比较方便.
namespace Clipify.Forms;
public static class RouterMap {
public const string Index = "/";
public const string VideoSplit = "/video-split";
public const string ExtractAudio = "/extract-audio";
}
导航栏的完整代码省略了,有兴趣的同学之间在 Github 上看完整代码吧.
这里记录一个老生常谈的问题,如何高亮当前菜单?
有两种方式:
在本文中我使用的是 NavLink 组件,类似这样:
当路径与菜单的 href 相同时,元素会自动加上 ActiveClass 里的 class,从而实现高亮当前菜单的效果.
<NavLink href="@RouterMap.ExtractAudio" ActiveClass="bg-gray-200">
<i class="fa-solid fa-music"></i>
<span>提取音频</span>
</NavLink>
因为篇幅关系省略了 TailwindCSS 的 class 。
目前是把 MediatR 用在了对话框的数据交互上.
因为要处理视频,所以需要一个打开文件的对话框,和一个选择输出目录的对话框.
Blazor 组件是运行在浏览器里的,浏览器自然也能打开文件,不过打开后程序只能拿到文件的 stream ,而我需要拿到文件在电脑里的存储路径,用于调用 ffmpeg 命令进行处理.
这种情况下只能使用 WinForms 的对话框控件了,Blazor 组件与 WinForms 处在同个进程,这种情况下,使用 MediatR 这类进程内消息队列就很合适了.
MediatR 支持两种类型的消息,分别是 。
一种是一对一,另一种是一对多.
我的用法是这样:
为了屏蔽细节和解耦,我封装了 DialogService,这样做的好处是可以进一步简化组件与 MediatR 之间的通信,确保所有与文件对话框相关的逻辑集中在一个地方,使代码更具可维护性和一致性.
public class DialogService {
private readonly IMediator _mediator;
public event Func<string, Task>? OnFileSelected;
public event Func<string, Task>? OnDirSelected;
public DialogService(IMediator mediator) {
_mediator = mediator;
}
public async Task<string> OpenFileAsync() {
return await _mediator.Send(new OpenFileRequest());
}
public async Task<string> OpenDirAsync() {
return await _mediator.Send(new OpenDirRequest());
}
public void NotifyFileSelected(string path) {
OnFileSelected?.Invoke(path);
}
public void NotifyDirSelected(string path) {
OnDirSelected?.Invoke(path);
}
}
其中有两个事件,分别是打开文件和选择目录。这样设计的好处有几点:
DialogService
,包括 MediatR 的请求和处理。这样可以在一个地方轻松维护代码,提高可读性和可维护性。以打开文件为例.
代码 Clipify.Forms/EventBus/Request/OpenFileRequest.cs 。
using Clipify.Forms.EventBus.Notification;
using MediatR;
namespace Clipify.Forms.EventBus.Request;
public class OpenFileRequest : IRequest<string> { }
public class OpenFileHandler : IRequestHandler<OpenFileRequest, string> {
private readonly IMediator _mediator;
private readonly FormMain _formMain;
public OpenFileHandler(FormMain formMain, IMediator mediator) {
_formMain = formMain;
_mediator = mediator;
}
public Task<string> Handle(OpenFileRequest request, CancellationToken cancellationToken) {
var result = _formMain.openFileDialog.ShowDialog();
if (result == DialogResult.OK) {
var path = _formMain.openFileDialog.FileName;
_mediator.Publish(new FileSelectedNoti {
SelectedPath = path
}, cancellationToken);
return Task.FromResult(path);
}
return Task.FromResult("");
}
}
收到 Request 之后,RequestHandler 里通过依赖注入拿到 MainForm 的实例,然后调用对话框拿到文件路径,再发送通知.
代码 Clipify.Forms/EventBus/Notification/FileSelectedNoti.cs 。
PS:其实也可以使用 Request 的返回值来拿到文件路径,不过我还是”多此一举“使用了 Notification 。
using Clipify.Forms.Services;
using MediatR;
namespace Clipify.Forms.EventBus.Notification;
public class FileSelectedNoti : INotification {
public string SelectedPath { get; set; }
}
public class FileSelectedHandler : INotificationHandler<FileSelectedNoti> {
private readonly DialogService _dialogService;
public FileSelectedHandler(DialogService dialogService) {
_dialogService = dialogService;
}
public Task Handle(FileSelectedNoti notification, CancellationToken cancellationToken) {
_dialogService.NotifyFileSelected(notification.SelectedPath);
return Task.CompletedTask;
}
}
这个代码很简单,就是调用了 DialogService 的事件处理器.
在开发 Clipify 工具时,视频处理的核心依赖于 ffmpeg,这是一款强大的多媒体处理工具。为了实现视频剪辑、音频提取等功能,我探索了多种与 ffmpeg 交互的方式,包括使用现有的 C# 库以及直接通过系统进程调用 ffmpeg.
经过研究,可以用这几种方式来实现.
前两种都是用第三方库,我就不太多介绍了,有兴趣的同学直接看官方文档就行。另外提一点,C# 这边的生态还是差了点,就算是1k多star的FFMpegCore也没啥文档,只有一个项目的 README;前面那个 FFmpeg.NET 就更不用说了,已经停更了,而且文档有些代码和实际使用还对不上.
不过这些都是对于 ffmpeg 的调用,自己实现也是没问题的。下面是简单的例子:
Process ffmpegProcess = new Process();
ffmpegProcess.StartInfo.FileName = "ffmpeg";
ffmpegProcess.StartInfo.Arguments = "-i input.mp4 -progress pipe:1 -f mp4 output.mp4";
ffmpegProcess.StartInfo.RedirectStandardOutput = true;
ffmpegProcess.StartInfo.UseShellExecute = false;
ffmpegProcess.StartInfo.CreateNoWindow = true;
ffmpegProcess.OutputDataReceived += (sender, e) => {
if (!string.IsNullOrEmpty(e.Data)) {
// 处理标准输出中的进度信息
Console.WriteLine(e.Data);
// 可以在这里解析 e.Data 以提取进度
}
};
ffmpegProcess.Start();
ffmpegProcess.BeginOutputReadLine();
ffmpegProcess.WaitForExit();
参数说明:
-progress pipe:1
:表示将进度信息输出到标准输出(stdout
,即控制台)。FFmpeg 将输出一系列结构化的键值对,表示当前进度的状态。pipe:1
:是 FFmpeg 中表示标准输出流的方式,pipe:0
表示标准输入(stdin
),pipe:1
表示标准输出(stdout
),pipe:2
表示标准错误(stderr
)。在 ffmpeg 的参数里加上 -progress pipe:1 ,FFmpeg 会输出类似于以下内容的进度信息:
frame=1000
fps=24.0
stream_0_0_q=28.0
bitrate=456.8kbits/s
total_size=1024000
out_time_us=42000000
out_time_ms=42000
out_time=00:00:42.000000
dup_frames=0
drop_frames=0
speed=2.00x
progress=continue
这样就可以简单的获取更详细的视频处理进度信息.
不过 FFmpeg.NET 的 onData 事件是无法获取这段信息的,一般会获取到类似这样的输出:
size= 16522KiB time=00:21:19.01 bitrate= 105.8kbits/s speed=68.9x
就算添加了参数,也只能获取这一行的信息,所以要详细信息的话只能自己调用 Process 来处理.
并且 FFmpeg.NET 的 OnProgress 事件是有问题的,只能获取到 ProcessedDuration 信息,其他的都没办法了,不知道是不是版本太老,不匹配新版 ffmpeg ,如果有需要可以自己写正则解析一下.
// 使用正则表达式提取各项信息
string sizePattern = @"size=\s*(\d+)(\w+)";
string timePattern = @"time=(\d{2}:\d{2}:\d{2}\.\d{2})";
string bitratePattern = @"bitrate=\s*(\d+\.\d+|\d+)(\w+)";
string speedPattern = @"speed=\s*(\d+\.\d+|\d+)x";
在 Clipify 中,视频缩略图是帮助用户快速预览视频的重要功能.
在本项目的开发中,我探索了几种不同的缩略图策略:
为了避免重复生成缩略图,我采用了基于 MD5 哈希的策略为每个视频生成唯一的缩略图文件名。这样可以确保同一视频即使在不同时间被访问,仍然可以使用缓存的缩略图,提升性能.
这部分代码集成在 VideoService 里面.
使用了 FFmpeg.NET 提供的生成缩略图功能(其实就是调用ffmpeg对视频进行截图),根据规则生成文件名,之后把缩略图文件保存到 wwwroot/temp/thumbnails 目录里面.
public async Task<string> GenerateThumbnailAsync(string videoPath, CancellationToken? cancellationToken = null) {
var inputFile = new InputFile(videoPath);
var tempThumbnailDir = Path.Combine(_environment.WebRootPath, "temp", "thumbnails");
if (!Directory.Exists(tempThumbnailDir)) {
Directory.CreateDirectory(tempThumbnailDir);
}
var filename = $"{GetFileMetadataMd5(videoPath)}.jpeg";
var outputPath = Path.Combine(tempThumbnailDir, filename);
var outputFile = new OutputFile(outputPath);
var opt = new ConversionOptions {
HideBanner = true,
HWAccelOutputFormatCopy = true,
MapMetadata = true,
};
if (!File.Exists(outputPath)) {
await FFmpeg.GetThumbnailAsync(inputFile, outputFile, cancellationToken ?? CancellationToken.None);
}
return $"temp/thumbnails/{filename}";
}
最直接的方式是对整个视频文件进行 MD5 哈希运算,将其生成的哈希值作为缩略图的文件名。然而,如果视频文件较大,频繁进行哈希计算可能带来显著的性能开销.
public static string GetFileMd5(string filePath) {
using var md5 = MD5.Create();
using var stream = File.OpenRead(filePath);
var hash = md5.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
优点:文件内容唯一性强,可以确保不同内容的视频不会生成相同的缩略图.
缺点:对于大型文件,MD5 计算耗时较长,影响性能。实测几个G的视频要花好几秒的时间.
为了提高性能,也可以仅对文件路径进行 MD5 计算。这种方式大大减少了计算量,适用于那些文件内容不变但需要频繁生成缩略图的场景。然而,当文件被移动或重命名时,尽管视频内容没有变化,生成的 MD5 值会不同,可能导致不必要的重复缩略图生成.
string filePathHash;
using (var md5 = MD5.Create()) {
var pathBytes = Encoding.UTF8.GetBytes(videoFilePath);
var hash = md5.ComputeHash(pathBytes);
filePathHash = BitConverter.ToString(hash).Replace("-", "").ToLower();
}
优点:高效,MD5 计算速度极快,适合频繁使用.
缺点:文件路径变动时,即使文件内容不变,仍会生成新缩略图,可能导致冗余的缩略图生成.
为了在路径变化和文件内容唯一性之间找到平衡,Clipify 还可以结合文件的其他属性,如文件名、修改时间等进行 MD5 计算。这样即使文件路径发生变化,只要文件内容和其属性不变,MD5 也不会变化,避免不必要的重复生成.
public static string GetFileMetadataMd5(string filePath) {
var fileName = Path.GetFileName(filePath);
var fileInfo = new FileInfo(filePath);
var metaData = fileName + fileInfo.LastWriteTimeUtc.ToString();
using var md5 = MD5.Create();
var metaBytes = System.Text.Encoding.UTF8.GetBytes(metaData);
var hash = md5.ComputeHash(metaBytes);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
优点:
缺点:需要结合多个文件属性,计算稍微复杂,但仍能有效提升性能.
在 Clipify 中,选择如何生成视频缩略图的哈希值需要在性能和唯一性之间做平衡.
对于较大的视频文件,直接对文件进行 MD5 计算虽然保证了内容的唯一性,但对性能影响较大.
而通过结合文件路径和文件属性来生成哈希值,可以减少性能消耗并避免冗余的缩略图生成.
在后续的版本中,可以考虑小文件使用文件内容生成MD5,大文件继续用综合路径和属性的方式来生成MD5.
目前是用 FFmpeg.Net 的 OnProgress 事件,保留小数点后两位 。
private async void OnProgress(object? sender, ConversionProgressEventArgs e) {
Status.Status = StatusEnum.Running;
Status.Progress = Math.Round(e.ProcessedDuration.TotalSeconds / MetaData.Duration.TotalSeconds * 100, 2);
await InvokeAsync(StateHasChanged);
}
如果要更详细的显示处理时的其他信息,可以参考前面的与FFmpeg交互部分.
在 Clipify 的设计过程中,我非常注重用户体验中的细节,尤其是如何让用户更直观、轻松地理解视频文件的属性。因此,除了基本的视频编辑功能,我还在界面上优化了文件大小和视频长度的显示方式.
本文选择了这两点来介绍:
视频文件通常较大,直接显示以字节(bytes)为单位的大小可能不够直观。为了提升用户体验,我选择了将文件大小转换为更常见的单位,如 KB、MB 或 GB,并使用四舍五入让显示更简洁.
例如,如果视频文件大小为 3,304,582 字节,则会显示为 3.30 MB。这样一来,用户不需要进行单位换算,直接可以看到文件的大致大小.
这里我写了一个扩展方法来实现.
public static class FileInfoExtensions {
public static string GetFriendlySize(this FileInfo fileInfo) {
string[] sizeUnits = { "Bytes", "KB", "MB", "GB", "TB" };
double fileSize = fileInfo.Length;
int unitIndex = 0;
while (fileSize >= 1024 && unitIndex < sizeUnits.Length - 1) {
fileSize /= 1024;
unitIndex++;
}
return $"{fileSize:F2} {sizeUnits[unitIndex]}";
}
}
效果:
对于视频文件的长度,直接以秒或毫秒显示并不友好。为了提供更直观的体验,我选择了将视频长度转换为格式化的时间显示,如 HH:mm:ss,让用户能够快速了解视频的时长.
例如,一个长 5 分钟 44 秒的视频,系统会显示为 00:05:44,而不是直接显示秒数(如 344 秒)。这种显示方式符合用户日常的认知习惯,让用户能更轻松地估计视频内容的时间跨度.
依然是使用扩展方法来实现(我甚至还写了英文版本) 。
public static class TimeSpanExtensions {
public static string ToFriendlyString(this TimeSpan timeSpan, string locale = "zh-cn") {
var parts = new List<string>();
switch (locale) {
case "zh-cn":
if (timeSpan.Days > 0)
parts.Add($"{timeSpan.Days}天");
if (timeSpan.Hours > 0)
parts.Add($"{timeSpan.Hours}小时");
if (timeSpan.Minutes > 0)
parts.Add($"{timeSpan.Minutes}分钟");
if (timeSpan.Seconds > 0)
parts.Add($"{timeSpan.Seconds}秒");
// 如果没有天、小时、分钟或秒的部分,显示为 0 秒
if (parts.Count == 0)
return "0 秒";
break;
default:
if (timeSpan.Days > 0)
parts.Add($"{timeSpan.Days} day{(timeSpan.Days > 1 ? "s" : "")}");
if (timeSpan.Hours > 0)
parts.Add($"{timeSpan.Hours} hour{(timeSpan.Hours > 1 ? "s" : "")}");
if (timeSpan.Minutes > 0)
parts.Add($"{timeSpan.Minutes} minute{(timeSpan.Minutes > 1 ? "s" : "")}");
if (timeSpan.Seconds > 0)
parts.Add($"{timeSpan.Seconds} second{(timeSpan.Seconds > 1 ? "s" : "")}");
// 如果没有天、小时、分钟或秒的部分,显示为 0 秒
if (parts.Count == 0)
return "0 seconds";
break;
}
return string.Join(", ", parts);
}
}
不过如果要固定格式的话,可以直接使用更简短的代码:
public static string FormatVideoDuration(TimeSpan duration)
{
return string.Format(
"{0:D2}:{1:D2}:{2:D2}",
duration.Hours,
duration.Minutes,
duration.Seconds);
}
细节决定体验。在 Clipify 的设计中,显示更友好的文件大小和视频长度是提升用户体验的关键步骤。通过将技术逻辑转化为直观的界面元素,用户可以更加轻松地操作视频文件,减少因信息不直观带来的困扰。这些小细节的优化将有助于提升整个工具的易用性和用户满意度.
相比之前的 QuickCutSharp,这个新工具在开发体验和界面设计上更加灵活,也更加适合我的需求。虽然起初尝试了一些其他的开发方案,如 Avalonia 和 MAUI,但最终因为环境复杂或平台不支持而放弃.
使用 Blazor 和 TailwindCSS 构建界面,既保持了熟悉的 C# 开发生态,又带来了现代化的前端体验,这让整个项目的开发更加顺畅。虽然 Clipify 目前只实现了部分功能,但我对其未来的发展充满期待。项目已经开源,希望能对有类似需求的开发者提供一些帮助.
最后此篇关于PC软件开发新体验!用BlazorHybrid打造简洁高效的视频处理工具的文章就讲到这里了,如果你想了解更多关于PC软件开发新体验!用BlazorHybrid打造简洁高效的视频处理工具的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我想知道有没有可能做 new PrintWriter(new BufferedWriter(new PrintWriter(s.getOutputStream, true))) 在 Java 中,s
我正在尝试使用 ConcurrentHashMap 初始化 ConcurrentHashMap private final ConcurrentHashMap > myMulitiConcurrent
我只是想知道两个不同的新对象初始化器之间是否有任何区别,还是仅仅是语法糖。 因此: Dim _StreamReader as New Streamreader(mystream) 与以下内容不同: D
在 C++ 中,以下两种动态对象创建之间的确切区别是什么: A* pA = new A; A* pA = new A(); 我做了一些测试,但似乎在这两种情况下,都调用了默认构造函数,并且只调用了它。
我已经阅读了其他帖子,但它们没有解决我的问题。环境为VB 2008(2.0 Framework)下面的代码在 xslt.Load 行导致 XSLT 编译错误下面是错误的输出。我将 XSLT 作为字符串
我想知道为什么alert(new Boolean(false))打印 false 而不是打印对象,因为 new Boolean 应该返回对象。如果我使用 console.log(new Boolean
本文实例讲述了Python装饰器用法。分享给大家供大家参考,具体如下: 写装饰器 装饰器只不过是一种函数,接收被装饰的可调用对象作为它的唯一参数,然后返回一个可调用对象(就像前面的简单例子) 注
我可以编写 YAML header 来使用 knit 为 R Markdown 文件生成多种输出格式吗?我无法重现 the original question with this title 的答案中
我可以编写一个YAML标头以使用knitr为R Markdown文件生成多种输出格式吗?我无法重现the original question with this title答案中描述的功能。 这个降价
我正在使用vars package可视化脉冲响应。示例: library(vars) Canada % names ir % `$`(irf) %>% `[[`(variables[e])) %>%
我有一个容器类,它有一个通用参数,该参数被限制到某个基类。提供给泛型的类型是基类约束的子类。子类使用方法隐藏(新)来更改基类方法的行为(不,我不能将其设为虚拟,因为它不是我的代码)。我的问题是"new
Java 在提示! cannot find symbol symbol : constructor Bar() location: class Bar JPanel panel =
在我的应用程序中,一个新的 Activity 从触摸按钮(而不是点击)开始,而且我没有抬起手指并希望在新的 Activity 中跟踪触摸的 Action 。第二个 Activity 中的触摸监听器不响
已关闭。此问题旨在寻求有关书籍、工具、软件库等的建议。不符合Stack Overflow guidelines .它目前不接受答案。 我们不允许提问寻求书籍、工具、软件库等的推荐。您可以编辑问题,
和我的last question ,我的程序无法检测到一个短语并将其与第一行以外的任何行匹配。但是,我已经解决并回答了。但现在我需要一个新的 def函数,它删除某个(给定 refName )联系人及其
这个问题在这里已经有了答案: Horizontal list items (7 个答案) 关闭 9 年前。
我想创建一个新的 float 类型,大小为 128 位,指数为 4 字节(32 位),小数为 12 字节(96 位),我该怎么做输入 C++,我将能够在其中进行输入、输出、+、-、*、/操作。 [我正
我在放置引用计数指针的实例时遇到问题 类到我的数组类中。使用调试器,似乎永远不会调用构造函数(这会扰乱引用计数并导致行中出现段错误)! 我的 push_back 函数是: void push_back
我在我们的代码库中发现了经典的新建/删除不匹配错误,如下所示: char *foo = new char[10]; // do something delete foo; // instead of
A *a = new A(); 这是创建一个指针还是一个对象? 我是一个 c++ 初学者,所以我想了解这个区别。 最佳答案 两者:您创建了一个新的 A 实例(一个对象),并创建了一个指向它的名为 a
我是一名优秀的程序员,十分优秀!