65.9K
CodeProject 正在变化。 阅读更多。
Home

MP3 重新排序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (9投票s)

2012 年 4 月 25 日

CPOL

5分钟阅读

viewsIcon

32087

downloadIcon

1693

为刻录到 CD 或 DVD 或加载到 MP3 播放器准备播放列表元素。

引言

这款 Windows 应用程序允许用户选择一个后缀名为 .m3u 的 Winamp 播放列表文件,并执行各种文件夹和文件名操作,以方便地创建一个目录,供播放列表中的 MP3 文件按顺序平坦化存储。然后,在刻录 MP3 CD 或 DVD 或加载到 MP3 播放器(如 Sansa Clip)后,MP3 文件将按照您想要的播放列表顺序播放。

背景

随着时间的推移,一个人的 MP3 收藏可能会变得非常庞大。MP3 文件可以按照各种不同的文件夹结构进行组织。在我的收藏中,有一个根文件夹“MP3”,其中包含子文件夹,如“艺术家”和“专辑”,然后是实际的 MP3 文件。但是,还有其他文件夹,如“杂项”、“古典”、“给爸爸”、“整张专辑”等。在整个文件夹树结构中穿插着播放列表文件,其中包含每个歌曲的歌曲顺序以及路径和文件名。

像 Winamp 这样的音乐播放器软件可以读取这些播放列表,并按照指定的顺序显示和播放歌曲。一些 DVD 播放器也可以播放刻录到 CD 或 DVD 中的 MP3 文件(数据格式)。以 MP3 格式,一张 CD 的内容大约可以存储到一张 MP3 数据 CD 中。

一些 DVD 播放器只能在扁平化的文件夹结构中播放 MP3,然后按字母数字顺序播放这些歌曲。

您可以选择以下四种操作之一。

  1. 复制
  2. 删除目标
  3. 移动
  4. 回退

对于复制和移动操作,还可以对播放列表中的所有文件执行额外的文件名操作。

  • 替换(复选框和两个用于查找和替换字符串的文本框)
  • 替换下划线
  • 插入编号
  • 在开头插入编号

选择操作后,“执行”按钮将被启用。 

对于复制或移动操作,将显示“预览”窗体,其中包含两个并排的复选列表框和其他一些控件。左侧显示当前文件名列表,右侧显示潜在的操作后的文件名。最初,两个列表框中的所有框都已被选中。

这给了您一个审查文件名的机会,然后您可以“取消”进行调整或“完成”。

当单击“完成”按钮时,文件将被移动或复制到目标文件夹,形成一个扁平化的文件结构,并带有操作后的文件名。只有选中的文件才会被处理。

完成后,将显示汇总结果。  

“删除目标”操作提供目录内容的摘要和一个取消选项。

“移动”操作会存储处理过的原始和修改后的完整文件名,因此即使您在移动后退出应用程序,“回退”操作仍然可以工作。  

操作速度

如果需要复制大量文件,复制可能会很慢。如果目标与源在同一卷上,移动和回退速度非常快。删除也很快。

解决方案

Visual Studio 2008 项目包含两个窗体 FormMP3Main.cs 和 FormPreview.cs,如上所示。主类是MP3Rearrange.cs,它从FormUtil.csRecursiveIO.cs、RegistryWrapper.cs、Logging.csBasicUtil.cs继承了五层。我的许多软件应用程序项目都使用了这些继承的类,特别是后三个。我尽量减少窗体中的代码量,并尽可能多地使用代码重用。

MP3Rearrange.cs 还使用了PersistControls.csXMLDictionary.csTwoString.cs 类。

主要的类构造顺序是:Program.cs 构造 FormMP3Main,它在其 OnLoad 事件中构造 MP3Rearrange。MP3Rearrange 再根据需要构造所有其他类的实例。

FormPreview 作为对话框从 MP3Rearrange.ShowPreview 运行。

PersistControls 类在 Persist() 方法中使用 RegistryWrapper 类将传递的窗体及其部分控件的详细信息存储到 Windows 注册表中。持久化的项目包括窗体的大小、位置和状态,以及任何文本框的内容和任何复选框的状态。Restore() 方法检索先前持久化的项目。Persist()Restore() 都调用 StartPersistControls(Form f) 方法,并将变量 bPersist 设置为相应的值。

private void StartPersistControls(Form f)
{
 // Store or Restore some elements of a Form and its child controls
 FormName = f.Name;

 if (bPersist && f.WindowState != FormWindowState.Minimized)
 {
  regWrap.PutString(FormName + "WindowState", f.WindowState.ToString());
 }

 if (f.WindowState == FormWindowState.Normal)
 {
  bool rv;
  if (bPersist)
  {
   // Persist Form size and location
   regWrap.PutInt(FormName + "Height", f.Height);
   regWrap.PutInt(FormName + "Width", f.Width);
   regWrap.PutInt(FormName + "LocX", f.Location.X);
   regWrap.PutInt(FormName + "LocY", f.Location.Y);
  }
  else
  {
   // Restore Form size and location
   int H, W, X, Y;
   rv = regWrap.GetIntKey(FormName + "Height", out H);
   if (rv)
   {
    f.WindowState =
     (FormWindowState) Enum.Parse(typeof (FormWindowState), 
            regWrap.GetStringKey(FormName + "WindowState", "Normal"));
            
    if (f.WindowState == FormWindowState.Normal)
    {
     regWrap.GetIntKey(FormName + "Width", out W);
     regWrap.GetIntKey(FormName + "LocX", out X);
     regWrap.GetIntKey(FormName + "LocY", out Y);
     Rectangle ScreenRect = Screen.FromControl(f).Bounds;
     var rect = new Rectangle(X, Y, W, H);
     // To avoid the form being restored to a location off the screen
     // we do the "Contains" test.
     // This can happen when using remote desktop, or using a 
     // different size screen.
     // On not contained stay with default values.

     if (ScreenRect.Contains(rect))
     {
      f.Height = H;
      f.Width = W;
      f.Location = new Point(X, Y);
     }
    }
   }
  }
 }

 PersistControl(f);
}

PersistControl 方法会递归调用自身。它负责持久化 TextBox 和 CheckBox 控件。

private void PersistControl(Control Paren)
{
 String CheckedDefault;
 foreach (Control ctrl in Paren.Controls)
 {
  if (bPersist)
  {
   // Persist
   if (ctrl is TextBox)
   {
    regWrap.PutString(FormName + "_" + ctrl.Name + "_Text", ctrl.Text);
   }

   if (ctrl is CheckBox)
   {
    var chkbox = (CheckBox) ctrl;
    regWrap.PutString(FormName + "_" + ctrl.Name + "_Checked", 
                      regWrap.BoolToString(chkbox.Checked));
   }
  }
  else
  {
   // Restore
   if (ctrl is TextBox)
   {
    ctrl.Text = regWrap.GetStringKey(FormName + "_" + ctrl.Name + "_Text",
                                     String.Empty);
   }
   else if (ctrl is CheckBox)
   {
    var chkbox = (CheckBox) ctrl;
    CheckedDefault = regWrap.BoolToString(chkbox.Checked);
    chkbox.Checked = regWrap.StringToBool(
           regWrap.GetStringKey(FormName + "_" + ctrl.Name + "_Checked",
                                CheckedDefault));
   }
  }

  if (ctrl.Controls.Count > 0)
  {
   // Recursive
   PersistControl(ctrl);
  }
 } // end foreach
}

对于“移动”操作,我们需要持久化原始的完整文件路径和修改后的目标文件名,以便能够通过“回退”操作进行恢复。

我们使用 XMLDictionary 类将播放列表的每个元素持久化到一个 XML 文件中。

TableName 和 NameSpace 在构造函数中指定。

下面显示了 WriteTbl 方法。

public void WriteTbl(Dictionary<String, String> htDictionary)
{
 // create table
 var table = new DataTable(TblName, NameSpace);
 table.MinimumCapacity = 10;
 table.CaseSensitive = false;

 Type aTyp = typeof (String);
 // define columns
 DataColumn col = table.Columns.Add(C1Name, aTyp);
 col.AllowDBNull = true;
 col = table.Columns.Add(C2Name, aTyp);
 col.AllowDBNull = true;

 String Key;
 String Val;
 // Add Rows
 DataRow row;
 Dictionary<string, string>.Enumerator en =
             htDictionary.GetEnumerator();
 while (en.MoveNext())
 {
  Key = en.Current.Key;
  Val = en.Current.Value;
  row = table.NewRow();
  row[0] = Key;
  row[1] = Val;
  table.Rows.Add(row);
 }
 // Write XML and XML Scheme
 String FFN = Path.Combine(AppDir, TblName);
 var fi = new FileInfo(FFN + ".xsd");
 if (!fi.Exists)
 {
  table.WriteXmlSchema(FFN + ".xsd");
 }
 table.WriteXml(FFN + ".xml");
}

下面显示了 ReadTbl 方法。

/// <summary>
///  Read data from an XML file and load into a Dictionary.
/// </summary>
/// <returns>Dictionary<String,String></returns>
public Dictionary<String, String} ReadTbl()
{
 var rv = new Dictionary<string, string>();
 String FFN = Path.Combine(AppDir, TblName);

 // Create DataTable
 var table = new DataTable();
 table.MinimumCapacity = 10;
 table.CaseSensitive = false;
 var fixsd = new FileInfo(FFN + ".xsd");
 var fixml = new FileInfo(FFN + ".xml");
 if (fixsd.Exists && fixml.Exists)
 {
  // Read data from files into DataTable
  table.ReadXmlSchema(FFN + ".xsd");
  table.ReadXml(FFN + ".xml");
  NameSpace = table.Namespace;
  TblName = table.TableName;
  C1Name = table.Columns[0].ToString();
  C2Name = table.Columns[1].ToString();

  String Key;
  String Val;
  int RowCnt = table.Rows.Count;
  // Load Dictionary from DataTable
  for (int i = 0; i < RowCnt; ++i)
  {
   Key = table.Rows[i][C1Name].ToString();
   Val = table.Rows[i][C2Name].ToString();
   rv.Add(Key, Val);
  }
 }
 return rv;
}

OnDestination 事件处理程序通过 FolderUtils.BrowseForDirectory 显示标准的 Windows FolderBrowserDialog,但有一个节省时间的区别。浏览开始的根文件夹是通过 SelectedPath 属性指定的。我将 SelectedPath 设置得尽可能接近之前的目标目录。

public String BrowseForDirectory(String InitPath, String Description)
{
 int ix;
 // set initial directory path as close as possible to last path
 while (!Directory.Exists(InitPath))
 {
  // directory does not exist
  ix = InitPath.LastIndexOf(@"\");
  if (ix > -1)
  {
   // move up one directory level
   InitPath = InitPath.Substring(0, ix);
  }
  else
  {
   break;
  }
 }

 var DirPicker = new FolderBrowserDialog();
 DirPicker.SelectedPath = InitPath;
 DirPicker.Description = Description;
 DialogResult dr = DirPicker.ShowDialog();

 String SelectedPath = String.Empty;
 if (dr == DialogResult.OK)
 {
  SelectedPath = DirPicker.SelectedPath;
 }

 DirPicker.Dispose();
 return SelectedPath;
}

MP3Rearrange.DeleteDir() 方法显示了目录内容的漂亮摘要。在获得批准后,目录将被删除。

RecursiveIO.StartRecurse 方法会遍历树中的所有目录和文件,在此过程中,会在每个合适的节点上调用虚拟方法 ProcessFileNameProcessDirName。此外,还会设置 FileCntDirCnt 属性。DeleteDir 还调用 BasicUtil 类中一些方便的小方法,如 NiceByteSizePluralizePluralizeYIES,这些方法使得目录摘要消息对用户更友好。

private void DeleteDir()
{
 // Delete directory and subdirectories and files therein, after confirmation.

 // Collect directory details
 StartRecurse(TBDestDir.Text);

 int dc = DirCnt - 1;
 String fmt = "Are you sure you want to delete all the contents ({0})";
 fmt += " of directory '{1}'  including {2} subdirector{3} and {4} file{5} ?";
 String sMsg = String.Format(fmt, NiceByteSize(DirSizeBytes), TBDestDir.Text,
                           dc, PluralizeYIES(dc), FileCnt, Pluralize(FileCnt));
 DialogResult rv = MessageBox.Show(sMsg, ProgName,
             MessageBoxButtons.OKCancel, MessageBoxIcon.Question);

 if (rv == DialogResult.OK)
 {
  Directory.Delete(TBDestDir.Text, true);
 }
}

DirSizeBytes 在下面重写的 ProcessFileName 方法中计算。

/// Override method in inherited class RecursiveIO.
/// Called for every file in the directory tree.
/// Calculate total size in bytes of all files in directory tree
protected override bool ProcessFileName(FileInfo fi)
{
 DirSizeBytes += fi.Length; // Size in Bytes
 return true;
}

重写 ProcessDirName 以避免日志文件中出现不必要的条目。

/// Override method in inherited class RecursiveIO.
/// Called for every directory in the directory tree.
protected override bool ProcessDirName(DirectoryInfo Dir)
{
 return true;
}

关注点

MP3Rearrange 的编写风格是我喜欢的,并且我认为它适用于独奏程序员的中小型项目。对于团队编程工作或大型项目,其他组织风格可能更好。

我用于实现应用程序运行之间持久化的技术非常有趣。

使用“工具提示”提供了更直观的图形用户界面。编写这个软件、记录它,以及为 codeproject.com 撰写这篇文章都很有趣。

已在 Windows 7、Vista、MS Server 2003 和 XP 上测试。

历史

第一版
© . All rights reserved.