音乐文件.kgm格式转.mp3格式WPF解决方案

  • 话不多说-先看效果
  • 制作背景
  • 关键技术
  • UI控件属性与后台数据绑定
  • 支持文本框拖动方式获取文件夹路径
  • 控制台输出内容监听
  • 数据进度条流畅动画


话不多说-先看效果

Github链接Gitee链接

鼠标拖动文件夹到灰色文本框即可取得文件路径,点击按钮也可以获得路径,获得的路径实时显示在灰色文本框。

python kgm转换为mp3 将kgm转成mp3_wpf


批量格式转换过程中能够实时显示成功转换多少、转换失败多少(99.9%都能转换成功,测试过几百个仅2个文件无法转码),以及转换完成的进度。

python kgm转换为mp3 将kgm转成mp3_wpf_02

制作背景

缘起前几天为了给妹妹的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);
        }
        }