音乐文件.kgm格式转.mp3格式WPF解决方案
话不多说-先看效果
Github链接
Gitee链接
鼠标拖动文件夹到灰色文本框即可取得文件路径,点击按钮也可以获得路径,获得的路径实时显示在灰色文本框。
批量格式转换过程中能够实时显示成功转换多少、转换失败多少(99.9%都能转换成功,测试过几百个仅2个文件无法转码),以及转换完成的进度。
制作背景
缘起前几天为了给妹妹的MP3下载周董的150首歌,为了妹妹不用手机耽误学习,狠心花了3块9开了会员,结果开完会员后发现下载的文件离开👖🐕后就不能播放,找遍全网无果不是要收费就是根本没法用,最后终于在GitHub上发现了一个东东,真的很Nice,有个Go编译的双击就可以运行,但必须将两个文件放在kgm文件同一级目录,不太喜欢。然后加上可能我被Windows洗脑了,一看到有个带命令启动的版本,视觉本能驱使我又套了个壳,套完壳后觉得手感还是可以😂
关键技术
UI控件属性与后台数据绑定
利用 wpf 支持数据驱动,可以提高响应速度。如果采用时间驱动,由于wpf前后台是分离的 UI 和 后台运行在不同线程,线程不同步虽然可以用invoke一下,但这样效率低啊,遇到需要传值的时候(基本都是这样的),就像用async里的await一样,势必会阻塞await后面的执行和因为。使用数据驱动的话就不用管这些事情了,把这些事交给编译器自己弄去吧,这样就实现了实时更新数据。
#region 属性的绑定
/// <summary>
/// 定义记录进度的字段及属性
/// </summary>
private string currentProgress = "";
public string CurrentProgress
{
get { return currentProgress; }
set
{
currentProgress = value;
OnPropertyChanged("CurrentProgress");
}
}
private string successFail = "";
public string SuccessFail
{
get { return successFail; }
set
{
successFail = value;
OnPropertyChanged("SuccessFail");
}
}
/// <summary>
/// 属性改变的事件委托
/// 实现INotifyPropertyChanged接口成员
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string PropertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
}
在xaml文件中对CurrentProgress、SuccessFail这两个属性绑定,只能对属性进行绑定,不能是字段,ElementName=mywindow 这里的元素名mywindow是因为我把CurrentProgress和SuccessFail属性直接放在这个窗体对象的分布类(也就是xaml对应的后台部分)中的,而我对这个xaml文件描述的窗体取得名字就是mywindow,而且绑定必须指定对象elementname和路径path,其中绑定非控件的时候可以省略path这个property,所以可以这样写
Text="{Binding CurrentProgress , ElementName=mywindow, Mode=OneWay}"
Text="{Binding SuccessFail, ElementName=mywindow, Mode=OneWay}"
支持文本框拖动方式获取文件夹路径
实现文本框的 PreviewDragOver 和 PreviewDrop 这两个事件函数,就可以支持文件夹拖动并,这里的实现的话只实现了一次拖一个文件夹,毕竟已经能满足实际情况了
/// <summary>
/// 鼠标拖动滑过的函数
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void tb_PreviewDragOver(object sender, DragEventArgs e)
{
e.Effects = DragDropEffects.Copy;
e.Handled = true;
}
/// <summary>
/// 鼠标落下时执行函数
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void tb1_PreviewDrop(object sender, DragEventArgs e)
{
foreach (string f in (string[])e.Data.GetData(DataFormats.FileDrop))
{
tb1.Text = f;
}
}
控制台输出内容监听
实现原理就是为cmd单独开一个进程(不是线程哦,进程是单独的一辆火车,线程只能是一辆火车的一节车厢),配置cmd可输出属性为true,然后调用进程BeginOutputReadLine()这个函数,让控制台疯狂输出,需要注意的是这两句话,因为委托(事件也是委托)可以托管多个相同函数,这样移除一次和添加一次可以防止重复执行 ProcessOutDataReceived 这个函数
curProcess.OutputDataReceived -= new DataReceivedEventHandler(ProcessOutDataReceived);
curProcess.OutputDataReceived += new DataReceivedEventHandler(ProcessOutDataReceived);
完整控制台输出监听代码:
#region 外部调用Exe并监听输出
System.Diagnostics.Process curProcess;
private void Transfer(string Exepath)
{
//新建一个进程用于监听
curProcess = new System.Diagnostics.Process();
curProcess.OutputDataReceived -= new DataReceivedEventHandler(ProcessOutDataReceived);
ProcessStartInfo p = new ProcessStartInfo();
p.FileName = "cmd.exe";
p.UseShellExecute = false;
p.WindowStyle = ProcessWindowStyle.Hidden;
p.CreateNoWindow = true;
p.RedirectStandardError = true;
p.RedirectStandardInput = true;
p.RedirectStandardOutput = true;
curProcess.StartInfo = p;
curProcess.Start();
curProcess.BeginOutputReadLine();
curProcess.OutputDataReceived += new DataReceivedEventHandler(ProcessOutDataReceived);
curProcess.StandardInput.WriteLine($"cd {Exepath}");
curProcess.StandardInput.WriteLine($"unlock -i {inputpath} -o {outputpath}");
}
/// <summary>
/// 进程接受事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void ProcessOutDataReceived(object sender, DataReceivedEventArgs e)
{
string str = e.Data;
if(str.Contains("successfully converted"))successCount++;
if (str.Contains("conversion failed")) failedCount++;
totalCount = successCount + failedCount;
}
#endregion
数据进度条流畅动画
实现了数据绑定没问题,但是数据刷新太快,比如数据跟新1000Hz,明显 UI 不可能刷新那么快,由于使用的是数据驱动 UI ,所以我们不能更新数据太快,放在定时器里面固定一个数据的刷新周期,为了保证和 UI 在一个进程,推荐使用DispatcherTimer 这个定时器
/// 定时器 Timer1
/// </summary>
DispatcherTimer Timer1 = new DispatcherTimer();
/// <summary>
/// 定时器 Timer1 触发函数
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Timer1_Tick(object sender, EventArgs e)
{
double temp2 = (totalCount / FilesOfSourceCounts) * 100;
ProgressBarHelper.SetAnimateTo(pb_import, (int)temp2);//定时更新进度条动画
ProgressBarHelper.SetAnimationDuration(pb_import,new TimeSpan(0,0,0,0,200));//定时更新进度条动画
CurrentProgress = Math.Min((int)temp2,100).ToString() + "%";//定时跟新绑定数据CurrentProgress " "
SuccessFail = "\xf058 "+ successCount + " \xf057 "+ failedCount;
if (Math.Min((int)temp2, 100) == 100)
Timer1.Stop();
/// <summary>
/// 初始化 Timer1
/// </summary>
private void Timer1_Init()
{
Timer1.Tick += Timer1_Tick;
Timer1.Interval = new TimeSpan(0, 0, 0, 0, 50);
}
}