前の10件 1  2  3  4  5  6  7  8  9  10  11

記事一覧

第04回 PDFファイルからテキスト情報を抜き出す

今回はstreamの中身を読んで、そこに画面表示するテキストがあったらそれを抜き出す。

PDFでのテキスト描画は以下のような形になっている。
このほかにも何通りかテキスト描画の表記方法があるが、今回はこの形式のみを処理対象とした。
またテキストのエンコーディングもshift-jis決め打ちとして処理している。

BT /F2 20.00 Tf ET					//フォント名とフォントサイズ
BT 320.88 354.34 Td (その者。のちに・・・) Tj ET		//描画位置xyとテキスト


まずは抜き出したテキストを保存するするクラスを作成します。
「プロジェクト」メニューかあら「クラスの追加」を選択し、「PDFPage.cs」というクラスを追加し、以下のようにします。
テキスト情報(フォント名、フォントサイズ、表示座標、テキスト)をリスト化して"ページ"として保持するだけのクラスです。

次にPDFTextReaderにstreamが来たらそれを読んで、情報を抜き出す処理を追加します。
本来であればオブジェクトを1つずつ読み解いてアクセスするべきものですが、何も気にせずに読み込んでいます。

実行すると、List Pages に情報が蓄積されます。
かなりいい加減ですがこれでPDFからの情報読み込み処理は完了です。

■PDFPage.cs
using System.Collections.Generic;

namespace Pdf2Pdf
{
	class PDFPosition
	{
		public float fX
		{
			get { return _fX; }
			set { _fX = value; }
		}
		float _fX = 0;

		public float fY
		{
			get { return _fY; }
			set { _fY = value; }
		}
		float _fY = 0;

		public PDFPosition()
		{
		}

		public PDFPosition(float x, float y)
		{
			_fX = x;
			_fY = y;
		}
	}


	class PDFText : PDFPosition
	{
		public string strText
		{
			get { return _strText; }
			set { _strText = value; }
		}
		string _strText = "";

		public string strFont
		{
			get { return _strFont; }
			set { _strFont = value; }
		}
		string _strFont = "";

		public float fPoint
		{
			get { return _fPoint; }
			set { _fPoint = value; }
		}
		float _fPoint = 0;


		public PDFText()
		{
		}

		public PDFText(float x, float y, string strText)
			: base(x, y)
		{
			_strText = strText;
		}

		public PDFText(string strFont, float fPoint, float x, float y, string strText)
			: base(x, y)
		{
			_strFont = strFont;
			_fPoint = fPoint;
			_strText = strText;
		}

	}


	class PDFPage
	{
		public List<object> Items
		{
			get { return _Items; }
			set { _Items = value; }
		}
		List<object> _Items = new List<object>();


		public PDFPage()
		{
		}
	}
}
■PDFTextReader.cs
						//トレーラー
						if (strObj == "trailer")
						{
							//トレーラーの情報は読み取らない
							//トレーラーはPDF末尾に配置されるから、これで読み込み終わり
							break;
						}
					}


					//オブジェクトの読み込み
					{
						byte[] data;
						string strAttribute;
						bool ret = ReadObject(br, strObj, out strAttribute, out data);
						if (ret && data != null && data.Length > 0)
						{
							//オブジェクトの処理

							PDFPage page = new PDFPage();

							//コンテンツの中身はテキストだと決めてかかってる。本当は駄目。
							//フォントが埋め込まれてたらそのフォントのバイナリに対してまでこれが呼ばれてしまう
							AnalyzeContents(data, page);

							//情報があったら、"ページ"として追加する
							if (page.Items.Count > 0)
								_listPages.Add(page);

						}
						if (ret == false)
						{
							Debug.Assert(false);
						}
					}


				}
			}
			return true;
		}



		public List<PDFPage> Pages
		{
			get { return _listPages; }
		}
		List<PDFPage> _listPages = new List<PDFPage>();





		/// <summary>
		/// テキスト情報の取り出し
		/// 
		/// 取り出したテキスト情報はPDFPageへ格納する
		///
		/// shift-jisのみ対応
		/// ( ) で囲まれたテキストのみを処理し、¥表記や、<>表記のテキストには対応しない
		/// </summary>
		void AnalyzeContents(byte[] pcbContents, PDFPage page)
		{
			byte[] data;

			//エスケープシーケンスを外す
			//「文章」以外の部分も処理することになるけど気にしない
			using (MemoryStream ms1 = new MemoryStream(pcbContents))
			using (BinaryReader br = new BinaryReader(ms1))
			using (MemoryStream ms2 = new MemoryStream())
			{
				while (ms1.Position != ms1.Length)
				{
					byte tmp = br.ReadByte();
					if (tmp != 0x5c)		//「\\」
					{
						ms2.WriteByte(tmp);
						continue;
					}
					tmp = br.ReadByte();
					ms2.WriteByte(tmp);
				}
				ms2.Flush();

				data = ms2.ToArray();
			}

			using (MemoryStream ms = new MemoryStream(data))
			using (PDFRawReader pr = new PDFRawReader(ms))
			{
				//"小説家になろう"の縦書きPDFはshift-jis
				Encoding enc = Encoding.GetEncoding("shift_jis");

				string strFontObj = "";
				float fFontPoint = 0;

				while (true)
				{
					if (ms.Position == ms.Length)
						break;

					string strLine = pr.ReadLine(enc, false);
					LogOut(strLine);

					//フォントobject名とフォントサイズの取得
					{
						Regex re = new Regex(@"BT /(.+) ([\d\.]+) Tf", RegexOptions.IgnoreCase);
						Match m = re.Match(strLine);
						while (m.Success)
						{
							try
							{
								strFontObj = m.Groups[1].Value;

								string strSize = m.Groups[2].Value;
								fFontPoint = float.Parse(strSize);
							}
							catch (Exception)
							{
								strFontObj = "";
								fFontPoint = 0;
							}

							m = m.NextMatch();
						}
					}


					//テキストの取得
					{
						//表示座標とテキストだけ抜き出す
						// ( ) で囲まれたテキストのみを処理し、¥表記や、<>表記のテキストには対応しない
						Regex re = new Regex(@"BT ([\d\.]+) ([\d\.]+) Td \((.+)\) Tj ET", RegexOptions.IgnoreCase);
						Match m = re.Match(strLine);
						while (m.Success)
						{
							try
							{
								string strX = m.Groups[1].Value;
								string strY = m.Groups[2].Value;
								string strText = m.Groups[3].Value;

								float x = float.Parse(strX);
								float y = float.Parse(strY);


								PDFText text = new PDFText(strFontObj, fFontPoint, x, y, strText);
								page.Items.Add(text);
							}
							catch (Exception)
							{
							}

							m = m.NextMatch();
						}
					}

				}
			}
		}

プロジェクトファイルをダウンロード

第03回 PDFファイルを読み込む

今回はPDFファイルを一気に読み込みます。

一般的なPDFの読み込み方としては、以下の流れになります。
1. PDFヘッダー/フッターをチェック
2. クロスリファレンス情報を読み込む
3. クロスリファレンスに沿ってオブジェクトを読み込む
4. オブジェクト内容に沿って表示

しかしクロスリファレンスは表記形式が複数あり、きちんと作るとこれだけで日が暮れそうなので省略。
以下の流れで読むことにします。
1. PDFヘッダーチェック
2. PDFフッターチェック
3. クロスリファレンスがあることをチェック(内容は見ない)
4. PDFヘッダー直後のファイル位置にseek
5. seek位置から「xxx 0 obj」という行を順々に探し、あったらオブジェクトとして読み込み開始
6. オブジェクトにstreamがあった場合はそれを読み込む
7. endobjeまで読み込む
8. 5に戻る

オブジェクトは例えば以下のような形式で表現され、「<<」が複数あったり、streamとして付加データが存在することもあります。
streamの「/FlateDecode」指定は、圧縮されたstreamデータを示し、C#の場合はDeflateStreamクラスで解凍できます。

2 0 obj
<<
/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
/Font << /F1 14 0 R /F2 16 0 R>>
/XObject << >>
>>
endobj

12 0 obj
<</Type /Catalog /Pages 1 0 R /OpenAction [3 0 R /FitH null] /PageLayout /OneColumn>>
endobj

20 0 obj
<</Filter /FlateDecode /Length 701>>
stream
endstream
xxxxx 圧縮されたバイナリデータ701bytes xxxxx
endobj


最初に読み込むPDFファイルを準備します。

小説家になろうにある以下の小説をピックアップしました。
その者。のちに・・・
PDFダウンロード元: http://pdfnovels.net/n9442cw/

ダウンロードしたファイルはCドライブのルートに保存しておきます。

ちょうどこの文章を作成しているときに読んでいたものです。
小説家になろうで典型的な俺TUEEEファンタジーハーレム物です。珍しく異世界転生属性はなし。
ある意味展開がありふれているので安心して?読めました。

順番が逆ですが、form1.cs内に後で作るPDF読み込みクラスを呼び出す処理を追加。

■Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Pdf2Pdf
{
	public partial class Form1 : Form
	{
		public Form1()
		{
			InitializeComponent();

			//以下を追加
			PDFTextReader pr = new PDFTextReader();
			bool ret = pr.Read(@"c:\N9442CW.pdf");
		}
	}
}


そしてPDFにアクセスするクラスを作成します。
「プロジェクト」メニューかあら「クラスの追加」を選択し、「PDFTextReader.cs」というクラスを追加、以下のようにすれば終わりです。
難しい処理は特になく、ごりごり読むだけです。

■PDFTextReader.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace Pdf2Pdf
{
	class PDFTextReader
	{
		void LogOut(string strText)
		{
			Debug.WriteLine(strText);
		}


		/// <summary>
		/// PDFヘッダー
		/// </summary>
		public string strPDFHeader
		{
			get { return _strPDFHeader; }
		}
		string _strPDFHeader = "";





		public bool Read(string strFile)
		{
			using (FileStream strm = new FileStream(strFile, FileMode.Open, FileAccess.Read))
			{
				return Read(strm);
			}
		}



		public bool Read(Stream strm)
		{
			using (PDFRawReader br = new PDFRawReader(strm))
			{
				long nAfterHeaderPos;
				//PDFヘッダーチェック
				{
					_strPDFHeader = br.ReadLine(false);
					LogOut(_strPDFHeader);
					if (_strPDFHeader.Substring(0, 7) != "%PDF-1.")
						return false;

					nAfterHeaderPos = br.BaseStream.Position;		//PDFヘッダー直後の位置を保存
				}


				//PDFフッターチェック
				{
					strm.Seek(0, SeekOrigin.End);
					string strHooter = br.ReadBackLine(false);
					if (strHooter != "%%EOF")
						return false;
				}

				//クロスリファレンス取得・・・はしない。startxref存在チェックだけ
				{
					//クロスリファレンス位置取得
					string strXrefPos = br.ReadBackLine(true);
					string strXrefHeader = br.ReadBackLine(true);
					if (strXrefHeader != "startxref")
						return false;
					long nXrefPos = long.Parse(strXrefPos);

					//クロスリファレンスまでシーク
					strm.Seek(nXrefPos, SeekOrigin.Begin);

					string strXrefStart = br.ReadLine(true);
					if (strXrefStart == "xref")
					{
						//通常のクロスリファレンス
					}
					else if (strXrefStart.IndexOf(" 0 obj") > 0)
					{
						//オブジェクトに入ったクロスリファレンス
						byte[] data;
						string strAttribute;
						bool ret = ReadObject(br, strXrefStart, out strAttribute, out data);
					}
					else
					{
						//未知のクロスリファレンス形式
						return false;
					}
				}


				//PDFヘッダー直後にシークしておく
				br.BaseStream.Seek(nAfterHeaderPos, SeekOrigin.Begin);


				while (true)
				{
					LogOut("");

					if (strm.Position == strm.Length)
						break;


					//obj開始
					string strObj;
					{
						strObj = br.ReadLine(true);
						LogOut(strObj);

						//「endobj」に続いて、次のobjの前にスペースだけの行があるpdfもある→xrefを参照せずに読もうとするのが悪い。本当はxref参照しないと駄目
						if (strObj.IndexOf("obj") < 0)
							continue;


						//クロスリファレンス
						if (strObj == "xref")
						{
							//セクションのあるxrefに未対応。この方法だとxref読み込み失敗することある

							int nCount = 0;
							string strSubsection = br.ReadLine(true);
							{
								Regex re = new Regex(@"\d+ (\d+)", RegexOptions.IgnoreCase);
								Match m = re.Match(strSubsection);
								if (m.Success)
									nCount = int.Parse(m.Groups[1].Value);
							}

							for (int i = 0; i < nCount; i++)
							{
								string strXref = br.ReadLine(true);
							}
							continue;
						}

						//トレーラー
						if (strObj == "trailer")
						{
							//トレーラーの情報は読み取らない
							//トレーラーはPDF末尾に配置されるから、これで読み込み終わり
							break;
						}
					}


					//オブジェクトの読み込み
					{
						byte[] data;
						string strAttribute;
						bool ret = ReadObject(br, strObj, out strAttribute, out data);
						if (ret && data != null && data.Length > 0)
						{
							//オブジェクトの処理

						}
						if (ret == false)
						{
							Debug.Assert(false);
						}
					}


				}
			}
			return true;
		}



		/// <summary>
		/// PDFのオブジェクト情報読み取り
		/// "123 0 obj" ~ "endobj" まで読み取る
		/// 
		/// strObjectHeaderは "123 0 obj" というようなオブジェクト開始文字列
		/// stream/endstream内の情報は解凍してpcbStreamで返す
		/// 「<< >>」で囲まれていた属性部分のテキストはstrAttributeですべて返す
		/// </summary>
		bool ReadObject(PDFRawReader br, string strObjectHeader, out string strAttribute, out byte[] pcbStream)
		{
			string strLine = strObjectHeader;
			pcbStream = null;
			strAttribute = "";

			LogOut(strLine);

			if (strLine.IndexOf(" 0 obj") < 0)
				return false;

			long nStartPosition = br.BaseStream.Position;

			//オブジェクトの属性読み取り
			int nLength = 0;
			bool bFlateDecode = false;

			int nTagCount = 0;
			while (true)
			{
				strLine = br.ReadLine(true);
				LogOut(strLine);

				int nFind = strLine.IndexOf("/Length ");
				if (nFind >= 0)
				{
					Regex re = new Regex(@"/Length (\d+)", RegexOptions.IgnoreCase);
					Match m = re.Match(strLine);
					if (m.Success)
						nLength = int.Parse(m.Groups[1].Value);
				}

				nFind = strLine.IndexOf("/FlateDecode");
				if (nFind >= 0)
					bFlateDecode = true;

				//属性情報の始点(<<)をカウント
				nFind = 0;
				while (nFind >= 0)
				{
					nFind = strLine.IndexOf("<<", nFind);
					if (nFind >= 0)
					{
						nTagCount++;
						nFind += 2;
					}
				}

				//属性情報の終点(>>)をカウント
				nFind = 0;
				while (true)
				{
					nFind = strLine.IndexOf(">>", nFind);
					if (nFind < 0)
						break;

					nTagCount--;
					nFind += 2;

					//属性情報がまだあるか
					if (nTagCount > 0)
						continue;

					//「>>」につづいて改行を挟まずに「stream」が開始することあるから、その場合はseek
					if (nFind < strLine.Length)
						br.BaseStream.Seek(-(strLine.Length - nFind + 2), SeekOrigin.Current);

					//属性情報が終わった
					break;
				}

				//属性情報が終わったかどうか
				if (nTagCount == 0)
					break;
			}

			// << >>で囲まれていた部分をすべて抜き出す
			{
				long nEndPosition = br.BaseStream.Position;

				br.BaseStream.Seek(nStartPosition, SeekOrigin.Begin);
				byte[] data = br.ReadBytes((int)(nEndPosition - nStartPosition));

				strAttribute = Encoding.ASCII.GetString(data);
			}



			//コンテンツがあるなら読み込む
			if (nLength > 0)
			{
				string strStart = br.ReadLine(true);
				if (strStart != "stream")
				{
					Debug.Assert(false);
					return false;
				}

				if (bFlateDecode)
				{
					//FlateDecodeヘッダー2バイト
					br.ReadByte();
					br.ReadByte();
					nLength -= 2;

					//FlateDecodeのコンテンツをすべて読み取る
					byte[] data = br.ReadBytes(nLength);
					if (data.Length != nLength)
						return false;

					//解凍
					pcbStream = DecodeDeflate(data);
				}
				else
				{
					//streamコンテンツ読み込み
					pcbStream = br.ReadBytes(nLength);
				}

				string strEnd;

				strEnd = br.ReadLine(false);		//streamデータ末尾の改行分を読む(改行コードは1or2bytes)
				if (strEnd == "endstream")
				{
					//ここに入ることは普通はないはず。入ったらstream末尾に改行コードなくendstreamしていたpdf
				}
				else
				{
					//endstreamの読み込み
					strEnd = br.ReadLine(true);
					if (strEnd != "endstream")
					{
						Debug.Assert(false);
						return false;
					}
				}
			}


			{
				string strObjEnd = br.ReadLine(true);
				if (strObjEnd != "endobj")
				{
					Debug.Assert(false);
					return false;
				}
			}

			return true;
		}




		/// <summary>
		/// Deflateをデコードする
		/// </summary>
		byte[] DecodeDeflate(byte[] data)
		{
			byte[] ret = new byte[0];
			byte[] buffer = new byte[1024];

			using (MemoryStream ms = new MemoryStream(data))
			using (DeflateStream ds = new DeflateStream(ms, CompressionMode.Decompress))
			{
				while (true)
				{
					int nRead = ds.Read(buffer, 0, buffer.Length);
					if (nRead <= 0)
						break;

					int nSize = ret.Length;
					Array.Resize(ref ret, nSize + nRead);

					Buffer.BlockCopy(buffer, 0, ret, nSize, nRead);
				}
			}

			return ret;
		}

	}
}


実行するとVisual Studioの出力ウインドウに以下のような形式でPDFの内容が出力されます。

%PDF-1.3

3 0 obj
3 0 obj
<</Type /Page
/Parent 1 0 R
/Resources 2 0 R
/Contents 4 0 R>>

4 0 obj
4 0 obj
<</Filter /FlateDecode /Length 236>>

5 0 obj
5 0 obj
<</Type /Page
/Parent 1 0 R
/Resources 2 0 R
/Contents 6 0 R>>

プロジェクトファイルをダウンロード

第02回 ファイルから行単位で読み込む

PDFファイルはバイナリデータも含みますが、基本的にはテキストファイルです。
改行コードが\r、\n、\r\nの3種類あり得るため、実際にPDFファイルへアクセスする前に改行コードを処理できるファイル読み込みクラスを作成します。

今回はVisual Studio 2012を利用しました。
「ファイルメニュー」の「新規作成」にある「プロジェクトの作成」を選択し、新しいプロジェクトを作成します。

テンプレートは「Visual C#」の「Windows」にある「Windowsフォームアプリケーション」を採用し、プロジェクト名は「Pdf2Pdf」としました。

次に「プロジェクト」メニューかあら「クラスの追加」を選択し、「PDFRawReader.cs」というクラスを追加し、中身を以下のようにして終わり。
今回はクラスを作っただけなので動作は一切ありません。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Pdf2Pdf
{
	class PDFRawReader : BinaryReader
	{
		public PDFRawReader(Stream strm)
			: base(strm)
		{
		}

		public char cbCommentChar
		{
			get { return _cbCommentChar; }
			set { _cbCommentChar = value; }
		}
		char _cbCommentChar = '%';


		/// <summary>
		/// 1行ずつ読み込む
		/// bSkipCommentLine == trueならコメント行(%から始まる行)は読み取らない
		/// </summary>
		public string ReadLine(bool bSkipCommentLine)
		{
			return ReadLine(Encoding.ASCII, bSkipCommentLine);
		}


		/// <summary>
		/// 1行ずつ読み込む
		/// bSkipCommentLine == trueならコメント行(%から始まる行)は読み取らない
		/// </summary>
		public string ReadLine(Encoding enc, bool bSkipCommentLine)
		{
			do
			{
				byte tmp;
				byte[] data = new byte[0];
				int n = 0;

				try
				{
					while (true)
					{
						//改行コードチェック(\r、\r\n、\nの3通りあり得る)
						tmp = ReadByte();
						if (tmp == 0x0d)	//\rなら、\r\nか\rかをチェック
						{
							tmp = ReadByte();
							if (tmp == 0x0a)		//\r\nだった
								break;

							//\r\nではなく、\rだったので一文字戻してから抜ける
							BaseStream.Seek(-1, SeekOrigin.Current);
							break;
						}
						if (tmp == 0x0a)
							break;

						Array.Resize(ref data, data.Length + 1);
						data[n] = tmp;
						n++;
					}
				}
				catch (Exception)
				{
				}

				string ret = enc.GetString(data);

				if (bSkipCommentLine == false || string.IsNullOrEmpty(ret) || ret[0] != _cbCommentChar)
					return ret;
			}
			while (true);
		}


		/// <summary>
		/// データを参照する(シーク位置は変えない)
		/// </summary>
		byte ReferByte(int nOffset)
		{
			byte ret;
			long current = BaseStream.Position;

			BaseStream.Seek(nOffset, SeekOrigin.Current);
			ret  = ReadByte();

			BaseStream.Seek(current, SeekOrigin.Begin);

			return ret;
		}



		public string ReadBackLine(bool bSkipCommentLine)
		{
			return ReadBackLine(Encoding.ASCII, bSkipCommentLine);
		}

		/// <summary>
		/// 現在のシーク位置に対して前の1行を読み込む
		/// ※どんどんファイル先頭側へシークしていく
		/// 
		/// bSkipCommentLine == trueならコメント行(%から始まる行)は読み取らない
		/// </summary>
		public string ReadBackLine(Encoding enc, bool bSkipCommentLine)
		{
			do
			{
				byte tmp;
				int nBreakLine = 0;
				int nReferPos = 0;

				List<byte> listData = new List<byte>();

				try
				{
					//直前の改行コードは無視する
					{
						nReferPos--;
						tmp = ReferByte(nReferPos);

						//改行コードチェック(\r(0x0d)、\r\n(0x0d0a)、\n(0x0a)の3通りあり得る)
						{
							if (tmp == 0x0a)	//\nなら、\r\nか\nかをチェック
							{
								byte tmp2 = ReferByte(nReferPos - 1);
								if (tmp2 == 0x0d)
								{
									//\r\nだった
									nReferPos--;
								}
								else
								{
									//\r\nではなく、\nだった
								}
							}
							else if (tmp == 0x0d)
							{
								//\rだった
							}
						}
					}


					while (nBreakLine == 0)
					{
						nReferPos--;

						tmp = ReferByte(nReferPos);

						//改行コードチェック(\r(0x0d)、\r\n(0x0d0a)、\n(0x0a)の3通りあり得る)
						{
							if (tmp == 0x0a)	//\nなら、\r\nか\nかをチェック
							{
								byte tmp2 = ReferByte(nReferPos - 1);
								if (tmp2 == 0x0d)
								{
									//\r\nだった
									nBreakLine++;
									continue;
								}
								else
								{
									//\r\nではなく、\nだった
									nBreakLine++;
									continue;
								}
							}
							else if (tmp == 0x0d)
							{
								//\rだった
								nBreakLine++;
								continue;
							}
						}

						listData.Insert(0, tmp);
					}
				}
				catch (Exception)
				{
				}

				BaseStream.Seek(nReferPos + 1, SeekOrigin.Current);

				string ret = enc.GetString(listData.ToArray());

				if (bSkipCommentLine == false || string.IsNullOrEmpty(ret) || ret[0] != _cbCommentChar)
					return ret;

				if (BaseStream.Position == 0)
					return ret;
			}
			while (true);
		}
	}
}

プロジェクトファイルをダウンロード

第01回 PDFファイルフォーマットの概要

PDFのファイル仕様はおおまかに以下の形式になっています。

%PDF1.xxx				//ファイルヘッダー
xxx 0 obj				//1つ目のオブジェクト開始
<<xxx>>					//1つ目のオブジェクトの属性値
endobj					//1つ目のオブジェクト終了
xxx 0 obj				//2つ目のオブジェクト開始
<<xxx /Length xxx>>		//2つ目のオブジェクトの属性値
stream					//2つ目のオブジェクトのstreamの開始
xxx						//2つ目のオブジェクトのstream内容(Lengthのバイト数分)
endstream				//2つ目のオブジェクトのstreamの終了
endobj					//2つ目のオブジェクト終了
.
.
.
xref					//クロスリファレンスの開始
0 xxx					//クロスリファレンスの数
0000000000 65535 f
xxxxxxxxxx 00000 n		//1つ目のオブジェクトの位置情報
xxxxxxxxxx 00000 n		//2つ目のオブジェクトの位置情報
xxxxxxxxxx 00000 n		//3つ目のオブジェクトの位置情報
xxxxxxxxxx 00000 n		//4つ目のオブジェクトの位置情報
trailer					//トレーラーの開始
<<xxx /Root xxx 0 R /Size xxx>>		//トレーラーの属性値
startxref
xxx						//クロスリファレンスの位置情報
%%EOF					//ファイルフッター


以下はおおまかな注意事項

・各情報の区切りには改行コード(「\r」、「\n」、「\r\n」の3種類どれか)が入る
・stream末尾(「endstream」の1~2バイト前)にも改行コードが入る
・オブジェクト情報がstreamとして格納されることもある
・stream情報は圧縮されることもある
・クロスリファレンス情報はstreamとしてどこかのオブジェクト内にバイナリ形式で格納されることもある(3.4.7 Cross-Reference Streams)
・クロスリファレンス情報はセクション分割されて表記されることもある(3.4.3 Cross-Reference Table example3.6)
・オブジェクト属性値の空白は省略や改行されることもある(例:「<</Type/Page>>」「<<\r/Type\r/Page\r>>」「<</Type /Page>>」)
・「%」から始まる行はコメント行(PDFヘッダー/フッターを除く)

・文字は「xyz」(そのまま書く)、「<78797A>」(16進数で文字コードを書く)、「\120\121\122」(10進数で文字コードを書く)のように複数の表現がある
・文字中の「(」「)」「\」はエスケープして「\(」「\)」「\\」のようにされるが、カッコはエスケープされないこともある
・文字中の改行コードの1バイト前に「\」がある場合は改行コードは無視する


簡単に要約すると「PDFの表記方法は複数あるため、PDFを読むのは大変」。
汎用的なPDF操作処理を行いたい場合は無理せずにAdobeのSDKやPDFのライブラリを使うのが無難です。


PDFの仕様書
Adobe Portable Document Format Version 1.7
http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/pdf_reference_1-7.pdf

2016年03月16日

第37回 レーティングに応じた重み付け再生する

今回はレーティングがついた曲の再生頻度を高める処理を追加する。

曲のレーティングだけなら再生する曲を決めるときに重み付けすればいいが、
アーティストのレーティングもあるとリアルタイムの重み付けは大変になりそうなので、
再生フォルダを読み込むときに重み付けデータ_listnRatingWeightItemを作成する形にした。
再生時にはGetNextIndex()でそれを加味して重み付けしている。

"重み"はGetRatingWeightPlayCount()で決め、今回は曲レーティングの5倍、アーティストレーティングの3倍で重み付けした。
(曲レーティング1なら5倍、曲レーティング5&アーティストレーティング5なら40倍の頻度で再生される)

■Form1.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.IO.Compression;
using System.Threading;
using System.Windows.Forms;
using WMPLib;

namespace MP3Player
{
	public partial class Form1 : Form
	{
		WindowsMediaPlayer _mediaPlayer = new WindowsMediaPlayer();

		System.Windows.Forms.Timer _timer = new System.Windows.Forms.Timer();

		public static Option _option = new Option();


		List<AudioItem> _listFiles;			//音楽ファイル一覧
		int _nCurrentIndex = -1;			//↑へのインデックス
		List<int> _listnRatingWeightItem = new List<int>();	//↑↑へのインデックス。レイティングで重み付けした数だけ格納		//追加


		List<int> _listPlayHistory = new List<int>();		//過去の再生曲
		List<int> _listPlayNext = new List<int>();			//未来の再生曲。なければランダム


		bool _bStopButtonPushed = false;	//「停止」ボタンを押したらtrue
		/// <summary>
		/// 設定フォルダから音楽ファイルリストを読み込む
		/// </summary>
		void CreateFileListAndPlay(bool bPlay)
		{
			buttonStop_Click(null, null);		//再生停止

			//再生リストのクリア
			_listPlayHistory.Clear();
			_listPlayNext.Clear();
			_listnRatingWeightItem.Clear();		//追加
			_nCurrentIndex = -1;

			_dlgCreateFileList.Init();
			_dlgCreateFileList.StartPosition = FormStartPosition.Manual;
			_dlgCreateFileList.Location = new Point(Location.X + 50, Location.Y + 50);
			_dlgCreateFileList.Show(this);

			Thread thread = new Thread(new ThreadStart(delegate
				{
					{
						DialogResult ret = MessageBox.Show("ファイル読み込みだけでなく、DB更新もしますか?\n(時間がかかります)", "ファイル読み込み", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
						if (ret == DialogResult.OK)
						{
							//DB更新
							MP3Info.RefreshAllInfo(false, _dlgCreateFileList);
						}
					}

					//mp3フォルダからの音楽ファイルリスト読み込み
					_listFiles = EnumFiles(_option.strFolder, _dlgCreateFileList, _listnRatingWeightItem);		//変更

					try
					{
						Invoke((MethodInvoker)delegate
						{
							_dlgCreateFileList.Close();
						});
					}
					catch (Exception)
					{
					}

					if (bPlay)
					{
						Invoke((MethodInvoker)delegate
						{
							Play();		//再生開始
						});
					}
				}));

			thread.Start();
		}
		/// <summary>
		/// 次の再生曲インデックスを決める
		/// </summary>
		int GetNextIndex()
		{
			if (_listFiles.Count == 0)
				return -1;

			int nIndex = 0;
			Random rnd = new Random(Environment.TickCount);

			bool bFound = false;

			for (int i = 0; i < 200; i++)		//200回曲検索試行
			{
				if (_listPlayNext.Count == 0)
				{
					//変更
					//未来の曲リストが空ならランダムで曲を決定
					//レーティングに応じた重み付けがされる
					nIndex = rnd.Next(0, _listFiles.Count + _listnRatingWeightItem.Count);

					if (nIndex >= _listFiles.Count)
						nIndex = _listnRatingWeightItem[nIndex - _listFiles.Count];
				}
				else
				{
					//未来の曲リストから次の曲を決定
					nIndex = _listPlayNext[0];
					_listPlayNext.RemoveAt(0);
				}

				//同じ曲は連続で演奏しない
				if (nIndex == _nCurrentIndex)
					continue;

				//NGファイル指定されていないかチェック
				if (_option.IsNGFile(_listFiles[nIndex]))
					continue;

				//mp3情報を取得
				string strTitle;
				string strArtist;
				string strAlbum;
				MP3Info.GetMp3Info(_listFiles[nIndex], out strTitle, out strArtist, out strAlbum);

				//アーティストがNG指定されていないかチェック
				if (_option.IsNGArtist(strArtist))
					continue;

				//曲名がNGワードを含まないかチェック
				if (_option.IsNGWord(strTitle))
					continue;

				//ファイル追加日時が再生条件を満たすかチェック
				if (_option.IsPlayDayTermOK(_listFiles[nIndex]) == false)
					continue;

				//曲レーティングが再生条件を満たすかチェック
				if (_option.IsPlayRatingOK(_listFiles[nIndex]) == false)
					continue;

				//アーティストレーティングが再生条件を満たすかチェック
				if (_option.IsPlayRatingArtistOK(strArtist) == false)
					continue;

				//いずれかのレーティングが再生条件を満たすかチェック
				if (_option.IsPlayRatingAnyOK(_listFiles[nIndex], strArtist) == false)
					continue;

				bFound = true;
				break;
			}

			if (bFound == false)
			{
				//次の再生曲を決定できなかった
				//過去の曲もしくはインデックスゼロを再生
				if (_listPlayHistory.Count > 0)
				{
					//過去の曲からランダムに決定
					nIndex = rnd.Next(0, _listPlayHistory.Count);
				}
				else
				{
					nIndex = 0;
				}
			}

			return nIndex;
		}



		/// <summary>
		/// フォルダ内のファイルを一覧して返す
		/// 
		/// dlg.bCancel==trueになったら処理を中断して抜ける
		/// </summary>
		List<AudioItem> EnumFiles(string strFolder, ProgressForm dlg = null, List<int> listnRatingWeightItem = null)		//変更
		{
			List<AudioItem> ret = new List<AudioItem>();

			//指定フォルダ以下の全子フォルダから全ファイルを抜き出す
			IEnumerable<string> listFiles = Directory.EnumerateFiles(strFolder, "*.*", SearchOption.AllDirectories);
			//↑の処理は遅いが途中で中断できない

			//追加
			AudioItem item;
			string strTitle;
			string strArtist;
			string strAlbum;
			int nCount;

			foreach (string strFile in listFiles)
			{
				if (dlg != null)
				{
					try
					{
						dlg.strProgress = "" + ret.Count;
						if (dlg.bCancel)
							break;
					}
					catch (Exception)
					{
					}
				}

				//見つかったファイルの拡張子を取り出し
				string strExt = Path.GetExtension(strFile).ToLower();
				if (strExt == "")
					continue;

				//mp3/m4aならそのまま追加
				if (strExt == ".mp3" || strExt == ".m4a")
				{
					//追加
					item = new AudioItem(strFile);

					MP3Info.GetMp3Info(item, out strTitle, out strArtist, out strAlbum);
					if (_option.IsNGFile(item) || _option.IsNGArtist(strArtist) || _option.IsNGWord(strTitle))
						continue;			//NGなら処理しない

					//レーティングで重みづけた再生回数リスト化
					if (listnRatingWeightItem != null)
					{
						nCount = GetRatingWeightPlayCount(item, strTitle, strArtist, strAlbum);
						for (int j = 0; j < nCount; j++)
						{
							listnRatingWeightItem.Add(ret.Count);		//ret.Count == この曲へのインデックス
						}
					}

					ret.Add(item);		//変更
					continue;
				}

				//ZIPファイルなら書庫に含まれるmp3/m4aファイルのエントリーを追加
				if (strExt == ".zip")
				{
					try
					{
						using (ZipArchive archive = ZipFile.OpenRead(strFile))
						{
							for (int i = 0; i < archive.Entries.Count; i++)
							{
								if (dlg != null)
								{
									try
									{
										dlg.strProgress = "" + ret.Count;
										if (dlg.bCancel)
											break;
									}
									catch (Exception)
									{
									}
								}

								string strExtension = Path.GetExtension(archive.Entries[i].FullName).ToLower();
								if (strExtension == ".mp3" || strExtension == ".m4a")
								{
									//追加
									item = new AudioItem(strFile, i);

									MP3Info.GetMp3Info(item, out strTitle, out strArtist, out strAlbum);
									if (_option.IsNGFile(item) || _option.IsNGArtist(strArtist) || _option.IsNGWord(strTitle))
										continue;			//NGなら処理しない

									//レーティングで重みづけた再生回数リスト化
									if (listnRatingWeightItem != null)
									{
										nCount = GetRatingWeightPlayCount(item, strTitle, strArtist, strAlbum);
										for (int j = 0; j < nCount; j++)
										{
											listnRatingWeightItem.Add(ret.Count);		//ret.Count == この曲へのインデックス
										}
									}

									ret.Add(item);		//変更
								}
							}
						}
					}
					catch (Exception)
					{
					}
				}

			}

			return ret;
		}



		//追加
		/// <summary>
		/// レーティングで重みづけた再生回数を決める
		/// </summary>
		int GetRatingWeightPlayCount(AudioItem item, string strTitle, string strArtist, string strAlbum)
		{
			int nRating = _option.GetRating(item);
			int nRatingArtist = _option.GetRatingArtist(strArtist);

			return (nRating * 5 + nRatingArtist * 3);
		}

プロジェクトファイルをダウンロード

2016年03月12日

第36回 ツールチップを表示する

今回はウインドウのサイズを少し小さくする。
すると長いタイトルだと表示しきれないため、マウスポインターを運べばツールチップで表示されるようにする。

ついでにトラックバーはクリックしてもクリック位置に移動してくれず、使い勝手が悪いためその部分も修正した。

■Form1.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.IO.Compression;
using System.Threading;
using System.Windows.Forms;
using WMPLib;

namespace MP3Player
{
	public partial class Form1 : Form
	{
		WindowsMediaPlayer _mediaPlayer = new WindowsMediaPlayer();

		System.Windows.Forms.Timer _timer = new System.Windows.Forms.Timer();

		public static Option _option = new Option();

		List<AudioItem> _listFiles;			//音楽ファイル一覧
		int _nCurrentIndex = -1;			//↑へのインデックス

		List<int> _listPlayHistory = new List<int>();		//過去の再生曲
		List<int> _listPlayNext = new List<int>();			//未来の再生曲。なければランダム

		bool _bStopButtonPushed = false;	//「停止」ボタンを押したらtrue

		ToolTip _wndToolTip = new ToolTip();		//追加

		public Form1()
		{
			InitializeComponent();

			//設定の読み込み
			Option.Load(out _option, Utility.GetExeFolder() + "option.txt");

			Utility.CreateTmpFolder();			//tmpフォルダの作成


			//ウインドウ表示位置設定
			//[Ctrl]キーが押されていないときのみ設定=表示位置を復元したくないときは[Ctrl]キーを押して起動する
			if ((Control.ModifierKeys & Keys.Control) != Keys.Control)
			{
				StartPosition = FormStartPosition.Manual;
				Location = _option.ptWindowPos;
			}


			Init_FormDragMove();			//タイトルバーを消しフォームのドラッグで移動可能にする
			Init_TasktrayIcon();			//タスクトレイへの常駐化
			Init_ContextMenu();				//フォーム右クリックメニューの設定

			Init_MediaPlayer();				//WindowsMediaPlayer関連の初期化


			//以下を追加
			_wndToolTip.AutoPopDelay = 5000;		//表示される時間
			_wndToolTip.InitialDelay = 200;			//表示されるまでの時間
			_wndToolTip.ReshowDelay = 200;
			_wndToolTip.ShowAlways = true;


			_option.FolderChanged += delegate
			{
				CreateFileListAndPlay(true);		//フォルダを読み込み、再生開始
			};
			_option.FolderChanged(null, null);		//フォルダを読み込み、再生開始
		}



		/// <summary>
		/// WindowsMediaPlayer関連の初期化
		/// </summary>
		void Init_MediaPlayer()
		{
			//再生位置の手動シーク処理(シークバーの操作でシーク)
			trackBarSeek.ValueChanged += delegate
			{
				if (trackBarSeek.Focused)
				{
					_mediaPlayer.controls.currentPosition = (double)trackBarSeek.Value / 100;
				}
			};

			//以下を追加
			//トラックバーのクリック位置に応じてシークさせる
			trackBarSeek.MouseDown += (sender, e) =>
			{
				try
				{
					int nValue = trackBarSeek.Maximum * e.X / (trackBarSeek.Width - trackBarSeek.Margin.Horizontal);
					if (nValue < trackBarSeek.Minimum)
						nValue = trackBarSeek.Minimum;
					if (nValue > trackBarSeek.Maximum)
						nValue = trackBarSeek.Maximum;
					trackBarSeek.Value = nValue;
				}
				catch (Exception)
				{
				}
			};


			//音量の調節(トラックバーの操作で音量変更)
			_mediaPlayer.settings.volume = _option.nVolume;
			trackBarVolume.Maximum = 100;			//音量はゼロから100
			trackBarVolume.Value = _mediaPlayer.settings.volume;
			trackBarVolume.ValueChanged += delegate
			{
				_option.nVolume = (int)trackBarVolume.Value;
				_mediaPlayer.settings.volume = _option.nVolume;
			};


			_mediaPlayer.PlayStateChange += delegate(int nNewState)
			{
				WMPPlayState state = (WMPPlayState)nNewState;

				switch (state)
				{
					case WMPPlayState.wmppsStopped:
						buttonPlay.Text = ">";
						trackBarSeek.Value = 0;
						break;

					case WMPPlayState.wmppsPaused:
						_timer.Stop();
						buttonPlay.Text = ">";
						break;

					case WMPPlayState.wmppsPlaying:
						_bStopButtonPushed = false;
						buttonPlay.Text = "||";
						_timer.Start();
						break;

					case WMPPlayState.wmppsTransitioning:
						break;
				}
			};
		/// <summary>
		/// 曲を再生する
		/// </summary>
		void Play(bool bPlayNext = true)
		{
			if (_listFiles.Count == 0)
				return;

			if (bPlayNext || _listPlayHistory.Count == 0)
			{
				//次の曲を再生する
				if (_nCurrentIndex >= 0)
				{
					_listPlayHistory.Add(_nCurrentIndex);					//今の曲を履歴として保存
					if (_listPlayHistory.Count > 100)						//履歴の最大保存数は100
						_listPlayHistory.RemoveAt(0);
				}

				_nCurrentIndex = GetNextIndex();
			}
			else
			{
				//前の曲を再生する
				if (_nCurrentIndex >= 0)
					_listPlayNext.Insert(0, _nCurrentIndex);					//今の曲を次の曲として保存
				_nCurrentIndex = _listPlayHistory[_listPlayHistory.Count - 1];	//一番最新の履歴曲を今の曲とする
				_listPlayHistory.RemoveAt(_listPlayHistory.Count - 1);			//一番最新の履歴曲を削除
			}

			Utility.CleanTempFolder();			//tmpフォルダ内の削除

			string strFile = _listFiles[_nCurrentIndex].strFile;
			if (_listFiles[_nCurrentIndex].nEntryIndex >= 0)
			{
				//zip書庫ならtmpフォルダに解凍する
				bool ret = Utility.ExtractFileFromZip(strFile, _listFiles[_nCurrentIndex].nEntryIndex, Utility.GetTmpFile(), out strFile);
				if (ret == false)
					return;
			}

			_mediaPlayer.URL = strFile;			//再生曲をセット
			_mediaPlayer.controls.play();

			//曲名/タイトルの表示
			{
				string strTitle;
				string strArtist;
				string strAlbum;

				MP3Info.GetMp3Info(_listFiles[_nCurrentIndex], out strTitle, out strArtist, out strAlbum);

				labelTitle.Text = strTitle;
				labelArtist.Text = strArtist;

				//以下を追加
				//ラベルへツールチップス設定
				_wndToolTip.SetToolTip(labelTitle, strTitle);
				_wndToolTip.SetToolTip(labelArtist, strArtist);

				ShowRating();
			}
		}

プロジェクトファイルをダウンロード

第35回 ソースコードを整理する

今回はごちゃごちゃしているソースコードを一部整理。

Option.nPlayRatingStart/Endのようなネーミングが頭悪いプロパティやメンバ変数の名前を変更、
Form1のコンストラクタ内にあった右クリックメニュー、タスクトレイ処理などをInit_~()という形の関数化、
Form1のFormClosed、FormClosing、Shownなどの関数化
を行いました。
変更はForm1.csとOption.cs。

プロジェクトファイルをダウンロード


第34回 データベース再構築をキャンセル可能にする

今回は曲情報を保管するためのデータベース再構築処理をスレッドに載せてキャンセル可能にします。

変更作業は「第29回 再生フォルダ読み込みをキャンセル可能にする」と同様です。
ついでに少しだけデータベースの処理を効率化させました。まだまだ無駄が多いですがその辺は放置。

■MP3Info.cs
		//追加
		/// <summary>
		/// DBにある情報の更新日時を取得
		/// 
		/// ZIPファイル向けにエントリーインデックスを指定せずに使えるようにしたもの
		/// </summary>
		static bool SQL_FindDateInfo(AudioItem item, out DateTime dtLastWrite)
		{
			//引数で渡されたstrFileは音楽フォルダーのパスを含んだものかもしれないので、その部分を除外
			string strFile = Form1._option.RemoveMP3FolderName(item.strFile);

			try
			{
				using (SQLiteCommand cmd = _conn.CreateCommand())
				{
					cmd.CommandText = @"SELECT lastwritedate, title, artist, album FROM TagInfo WHERE file=@file;";
					cmd.Parameters.Add(new SQLiteParameter("@file", strFile));
					cmd.Prepare();

					using (SQLiteDataReader reader = cmd.ExecuteReader())
					{
						while (reader.Read())
						{
							try
							{
								dtLastWrite = new DateTime(reader.GetInt64(0));
								return true;
							}
							catch (Exception)
							{
							}
						}
					}
				}
			}
			catch (Exception)
			{
			}

			dtLastWrite = DateTime.MinValue;
			return false;
		}


		/// <summary>
		/// DBへのデータ保存
		/// </summary>
		static bool SQL_SetTagInfo(AudioItem item, DateTime dtLastWrite, string strTitle, string strArtist, string strAlbum)
		{
			//引数で渡されたstrFileは音楽フォルダーのパスを含んだものかもしれないので、その部分を除外
			string strFile = Form1._option.RemoveMP3FolderName(item.strFile);

			try
			{
				int nID = -1;
				DateTime dtLastWriteDB = DateTime.MinValue;		//追加

				//DB中にデータがすでにあるかどうかチェック。あればIDと日時を取得
				using (SQLiteCommand cmd = _conn.CreateCommand())
				{
					cmd.CommandText = @"SELECT id, lastwritedate FROM TagInfo WHERE file=@file and entryindex=@entryindex;";		//変更
					cmd.Parameters.Add(new SQLiteParameter("@file", strFile));
					cmd.Parameters.Add(new SQLiteParameter("@entryindex", item.nEntryIndex));
					cmd.Prepare();

					using (SQLiteDataReader reader = cmd.ExecuteReader())
					{
						while (reader.Read())
						{
							try
							{
								nID = reader.GetInt32(0);
								dtLastWriteDB = new DateTime(reader.GetInt64(1));		//追加
								break;
							}
							catch (Exception)
							{
							}
						}
					}
				}

				//IDがなかった=データがない=新規追加
				if (nID < 0)
					return SQL_AddNewTagInfo(item, dtLastWrite, strTitle, strArtist, strAlbum);

				//変更
				//IDが見つかった=データがある=古いなら更新
				if (dtLastWriteDB < dtLastWrite)
					return SQL_UpdateTagInfo(nID, dtLastWrite, strTitle, strArtist, strAlbum);
				else
					return true;		//更新の必要なし
			}
			catch (Exception)
			{
			}

			return false;
		}



		//変更
		/// <summary>
		/// DB情報の構築
		/// </summary>
		public static int RefreshAllInfo(bool bAllClear, ProgressForm dlg)		//変更
		{
			int nCount = 0;

			// サブ・ディレクトも含める
			IEnumerable<string> listFiles = Directory.EnumerateFiles(Form1._option.strFolder, "*.*", SearchOption.AllDirectories);

			bool ret;
			string strArtist;
			string strTitle;
			string strAlbum;

			if (bAllClear)
			{
				//既存のDBデータを全削除
				try
				{
					using (SQLiteCommand cmd = _conn.CreateCommand())
					{
						cmd.CommandText = @"TRUNCATE TABLE TagInfo;";
						cmd.ExecuteNonQuery();
					}
				}
				catch (Exception)
				{
				}
			}


			foreach (string strFile in listFiles)
			{
				//追加
				if (dlg != null)
				{
					if (dlg.bCancel)
						break;
					dlg.strProgress = "" + nCount;
				}

				string strExt = Path.GetExtension(strFile).ToLower();
				if (strExt == "")
					continue;

				if (strExt == ".mp3" || strExt == ".m4a")
				{
					//タグ情報読み込み
					ret = GetMp3Info(strFile, out strTitle, out strArtist, out strAlbum);
					if (ret)
					{
						DateTime dtLastWrite = File.GetLastWriteTime(strFile);

						//DBへ追加
						if (bAllClear)
							ret = SQL_AddNewTagInfo(new AudioItem(strFile), dtLastWrite, strTitle, strArtist, strAlbum);
						else
							ret = SQL_SetTagInfo(new AudioItem(strFile), dtLastWrite, strTitle, strArtist, strAlbum);
						if (ret)
							nCount++;
					}
					continue;
				}

				//ZIPファイルなら再生できそうな全エントリーを追加
				if (strExt == ".zip")
				{
					try
					{
						DateTime dtLastWriteDB;		//追加
						DateTime dtLastWrite = File.GetLastWriteTime(strFile);

						//追加
						if (bAllClear == false)
						{
							SQL_FindDateInfo(new AudioItem(strFile), out dtLastWriteDB);
							if (dtLastWriteDB >= dtLastWrite)
								continue;		//情報更新の必要なし
						}

						using (ZipArchive archive = ZipFile.OpenRead(strFile))
						{
							for (int i = 0; i < archive.Entries.Count; i++)
							{
								//追加
								if (dlg != null)
								{
									if (dlg.bCancel)
										break;
									dlg.strProgress = "" + nCount;
								}

								strExt = Path.GetExtension(archive.Entries[i].FullName).ToLower();
								if (strExt == ".mp3" || strExt == ".m4a")
								{
									try
									{
										//解凍
										string strTmpFile = Utility.GetTmpFile() + strExt;
										archive.Entries[i].ExtractToFile(strTmpFile, true);

										//タグ情報読み込み
										ret = GetMp3Info(strTmpFile, out strTitle, out strArtist, out strAlbum);
										if (ret)
										{
											//DBへ追加
											if (bAllClear)
												ret = SQL_AddNewTagInfo(new AudioItem(strFile, i), dtLastWrite, strTitle, strArtist, strAlbum);
											else
												ret = SQL_SetTagInfo(new AudioItem(strFile, i), dtLastWrite, strTitle, strArtist, strAlbum);
											if (ret)
												nCount++;
										}

										//解凍したファイルの削除
										File.Delete(strTmpFile);
									}
									catch (Exception)
									{
									}
								}
							}
						}
					}
					catch (Exception)
					{
					}
				}

			}
			return nCount;
		}

■Form1.cs
sing System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.IO.Compression;
using System.Threading;
using System.Windows.Forms;
using WMPLib;

namespace MP3Player
{
	public partial class Form1 : Form
	{
		WindowsMediaPlayer _mediaPlayer = new WindowsMediaPlayer();

		System.Windows.Forms.Timer _timer = new System.Windows.Forms.Timer();

		public static Option _option = new Option();


		List<AudioItem> _listFiles;			//音楽ファイル一覧
		int _nCurrentIndex = -1;			//↑へのインデックス


		List<int> _listPlayHistory = new List<int>();		//過去の再生曲
		List<int> _listPlayNext = new List<int>();			//未来の再生曲。なければランダム


		bool _bStopButtonPushed = false;	//「停止」ボタンを押したらtrue

		public Form1()
		{
			InitializeComponent();

			//設定の読み込み
			Option.Load(out _option, Utility.GetExeFolder() + "option.txt");

			Utility.CreateTmpFolder();			//tmpフォルダの作成


			//ウインドウ表示位置設定
			//[Ctrl]キーが押されていないときのみ設定=表示位置を復元したくないときは[Ctrl]キーを押して起動する
			if ((Control.ModifierKeys & Keys.Control) != Keys.Control)
			{
				StartPosition = FormStartPosition.Manual;
				Location = _option.ptWindowPos;
			}


			//削除
			////DBに情報がなければ再構築する=>時間が非常にかかる!
			//if (MP3Info.GetCount() == 0)
			//{
			//	MP3Info.RefreshAllInfo(true);
			//}
		ProgressForm _dlgCreateFileList = new ProgressForm("再生フォルダの読み込み中");


		/// <summary>
		/// 設定フォルダから音楽ファイルリストを読み込む
		/// </summary>
		void CreateFileListAndPlay(bool bPlay)
		{
			buttonStop_Click(null, null);		//再生停止

			//再生リストのクリア
			_listPlayHistory.Clear();
			_listPlayNext.Clear();
			_nCurrentIndex = -1;

			_dlgCreateFileList.Init();
			_dlgCreateFileList.StartPosition = FormStartPosition.Manual;
			_dlgCreateFileList.Location = new Point(Location.X + 50, Location.Y + 50);
			_dlgCreateFileList.Show(this);

			Thread thread = new Thread(new ThreadStart(delegate
				{
					//追加
					{
						DialogResult ret = MessageBox.Show("ファイル読み込みだけでなく、DB更新もしますか?\n(時間がかかります)", "ファイル読み込み", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
						if (ret == DialogResult.OK)
						{
							//DB更新
							MP3Info.RefreshAllInfo(false, _dlgCreateFileList);
						}
					}

					//mp3フォルダからの音楽ファイルリスト読み込み
					_listFiles = EnumFiles(_option.strFolder, _dlgCreateFileList);

					try
					{
						Invoke((MethodInvoker)delegate
						{
							_dlgCreateFileList.Close();
						});
					}
					catch (Exception)
					{
					}

					if (bPlay)
					{
						Invoke((MethodInvoker)delegate
						{
							Play();		//再生開始
						});
					}
				}));

			thread.Start();
		}

プロジェクトファイルをダウンロード

第33回 プログレスウインドウの不都合を解消する

以前「第29回 再生フォルダ読み込みをキャンセル可能にする」でプログレスウインドウを用意した。
今回はこのウインドウで不都合が生じる部分を修正する。

例えばキャンセルボタンではなくタイトルバーの「☓」ボタンからプログレスウインドウを閉じたり、
プログレスウインドウが開いているときにメインウインドウを終了したり、
プログレスウインドウが開いているときにさらに再生フォルダの変更を行ったり
した場合など、意図しない操作をするとすぐにフリーズする。
そのためそれらの操作を禁止する。

■ProgressDlg.cs
using System;
using System.Drawing;			//追加
using System.Windows.Forms;

namespace MP3Player
{
	public partial class ProgressForm : Form
	{
		public bool bCancel
		{
			get { return _bCancel; }
		}
		bool _bCancel = false;

		public EventHandler<EventArgs> Canceled = null;

		public string strProgress
		{
			set
			{
				try
				{
					Invoke((MethodInvoker)delegate
					{
						labelProgress.Text = value;
					});
				}
				catch (Exception)
				{
				}
			}
		}

		public ProgressForm(string strTitle)
		{
			InitializeComponent();
			Text = strTitle;

			//追加
			ControlBox = false;		//タイトルバーを消す
			FormBorderStyle = FormBorderStyle.FixedSingle;

			//追加
			//フォームのドラッグでウインドウを移動する
			{
				bool _bMoving = false;
				Point _ptMouseDown = new Point(-1, -1);
				Point _ptMouseDownWindowPos = new Point(-1, -1);

				MouseDown += (sender, e) =>
				{
					if (e.Button != MouseButtons.Left)
						return;

					_ptMouseDown.X = e.X;
					_ptMouseDown.Y = e.Y;
					_ptMouseDown = PointToScreen(_ptMouseDown);
					_ptMouseDownWindowPos.X = this.Left;
					_ptMouseDownWindowPos.Y = this.Top;
					_bMoving = true;
				};

				MouseMove += (sender, e) =>
				{
					if (_bMoving)
					{
						Point tmp = new Point(e.X, e.Y);
						tmp = PointToScreen(tmp);

						this.Left = _ptMouseDownWindowPos.X - _ptMouseDown.X + tmp.X;
						this.Top = _ptMouseDownWindowPos.Y - _ptMouseDown.Y + tmp.Y;
					}
				};

				MouseUp += (sender, e) =>
				{
					_bMoving = false;
					_ptMouseDown.X = -1;
					_ptMouseDown.Y = -1;
				};
			}
		}

		//以下を追加
		public void Init()
		{
			_bCancel = false;
		}
■Form1.cs
			//フォーム右クリックメニューの設定
			{
				ContextMenuStrip menu = new ContextMenuStrip();

				ToolStripItem[] stripItem = new ToolStripMenuItem[]
				{
					new ToolStripMenuItem("曲レーティング",null,new ToolStripMenuItem[]
					{
						new ToolStripMenuItem("なし", null, delegate
							{
								SetRating(0);
							}),
						new ToolStripMenuItem("★", null, delegate
							{
								SetRating(1);
							}),
						new ToolStripMenuItem("★★", null, delegate
							{
								SetRating(2);
							}),
						new ToolStripMenuItem("★★★", null, delegate
							{
								SetRating(3);
							}),
						new ToolStripMenuItem("★★★★", null, delegate
							{
								SetRating(4);
							}),
						new ToolStripMenuItem("★★★★★", null, delegate
							{
								SetRating(5);
							}),
					}),
					new ToolStripMenuItem("アーティストレーティング",null,new ToolStripMenuItem[]
					{
						new ToolStripMenuItem("なし", null, delegate
							{
								SetRatingArtist(0);
							}),
						new ToolStripMenuItem("★", null, delegate
							{
								SetRatingArtist(1);
							}),
						new ToolStripMenuItem("★★", null, delegate
							{
								SetRatingArtist(2);
							}),
						new ToolStripMenuItem("★★★", null, delegate
							{
								SetRatingArtist(3);
							}),
						new ToolStripMenuItem("★★★★", null, delegate
							{
								SetRatingArtist(4);
							}),
						new ToolStripMenuItem("★★★★★", null, delegate
							{
								SetRatingArtist(5);
							}),
					}),
					new ToolStripMenuItem("NG登録", null, delegate
						{
							if (_nCurrentIndex >= 0)
							{
								AddNGForm dlg = new AddNGForm(_listFiles[_nCurrentIndex], labelTitle.Text, labelArtist.Text);
								dlg.StartPosition = FormStartPosition.Manual;
								dlg.Location = new Point(Location.X + 50, Location.Y + 50);
								dlg.ShowDialog();
							}
						}),
					new ToolStripMenuItem("再生設定", null, delegate
						{
							//追加
							//読み込み中に再生フォルダ変更されるとやっかいだから読込中は設定不可に
							if (_dlgCreateFileList.Visible)
								return;

							PlayConditionForm dlg = new PlayConditionForm();
							dlg.ShowDialog(this);
						}),
					new ToolStripMenuItem("エクスプローラー", null, delegate
						{
							if (_nCurrentIndex >= 0)
							{
								Process.Start("explorer.exe","/select,\"" + _listFiles[_nCurrentIndex].strFile + "\"");
							}
						}),
					new ToolStripMenuItem("最小化", null, delegate
						{
							if (WindowState == FormWindowState.Normal)
								WindowState = FormWindowState.Minimized;
						}),
					new ToolStripMenuItem("終了", null, delegate
						{
							Close();
						}),
				};
				menu.Items.AddRange(stripItem);

				ContextMenuStrip = menu;
			}
			_option.FolderChanged += delegate
			{
				CreateFileListAndPlay(true);		//フォルダを読み込み、再生開始
			};
			_option.FolderChanged(null, null);		//フォルダを読み込み、再生開始


			//以下を追加
			FormClosing += (sender, e) =>
			{
				//ファイルリスト読み込み中なら終了キャンセル
				if (_dlgCreateFileList.Visible)
					e.Cancel = true;
			};
		}


		//移動/名前変更
		ProgressForm _dlgCreateFileList = new ProgressForm("再生フォルダの読み込み中");


		/// <summary>
		/// 設定フォルダから音楽ファイルリストを読み込む
		/// </summary>
		void CreateFileListAndPlay(bool bPlay)
		{
			buttonStop_Click(null, null);		//再生停止

			//再生リストのクリア
			_listPlayHistory.Clear();
			_listPlayNext.Clear();
			_nCurrentIndex = -1;

			//移動
			//ProgressForm dlg = new ProgressForm("再生フォルダの読み込み中");
			_dlgCreateFileList.Init();		//追加
			_dlgCreateFileList.StartPosition = FormStartPosition.Manual;
			_dlgCreateFileList.Location = new Point(Location.X + 50, Location.Y + 50);
			_dlgCreateFileList.Show(this);

			Thread thread = new Thread(new ThreadStart(delegate
				{
					//mp3フォルダからの音楽ファイルリスト読み込み
					_listFiles = EnumFiles(_option.strFolder, _dlgCreateFileList);

					try
					{
						Invoke((MethodInvoker)delegate
						{
							_dlgCreateFileList.Close();
						});
					}
					catch (Exception)
					{
					}

					if (bPlay)
					{
						Invoke((MethodInvoker)delegate
						{
							Play();		//再生開始
						});
					}
				}));

			thread.Start();
		}

プロジェクトファイルをダウンロード

2016年03月11日

第32回 タスクトレイに常駐させる

今回はタスクバーの右側、通知領域となるタスクトレイにアイコンを追加し、そのダブルクリックで最小化/元に戻すができるようにする。

まずタスクトレイに表示するアイコンをプロジェクトに追加する。
「プロジェクト」メニューから「既存項目の追加」を開き、適当なアイコンファイルを追加する。
今回はVisual Studio 2008付属の再配布可能アイコンのAudioFile.icoを利用した。

次にフォームへ通知アイコンコントロールを追加する。
Form1のデザインを開き、「NotifyIcon」を配置する。名前はデフォルト「notifyIcon1」のまま。
そしてプロパティを開き、Iconプロパティにプロジェクトフォルダ内のアイコンを指定する。
さらにTextプロパティは「MP3Player」とした。

■Form1.cs
		public Form1()
		{
			InitializeComponent();

			//設定の読み込み
			Option.Load(out _option, Utility.GetExeFolder() + "option.txt");

			Utility.CreateTmpFolder();			//tmpフォルダの作成


			//ウインドウ表示位置設定
			//[Ctrl]キーが押されていないときのみ設定=表示位置を復元したくないときは[Ctrl]キーを押して起動する
			if ((Control.ModifierKeys & Keys.Control) != Keys.Control)
			{
				StartPosition = FormStartPosition.Manual;
				Location = _option.ptWindowPos;
			}


			//DBに情報がなければ再構築する=>時間が非常にかかる!
			if (MP3Info.GetCount() == 0)
			{
				MP3Info.RefreshAllInfo(true);
			}


			ControlBox = false;		//タイトルバーを消す
			FormBorderStyle = FormBorderStyle.None;


			//フォームのドラッグでウインドウを移動する
			{
				bool _bMoving = false;
				Point _ptMouseDown = new Point(-1, -1);
				Point _ptMouseDownWindowPos = new Point(-1, -1);

				MouseDown += (sender, e) =>
				{
					if (e.Button != MouseButtons.Left)
						return;

					_ptMouseDown.X = e.X;
					_ptMouseDown.Y = e.Y;
					_ptMouseDown = PointToScreen(_ptMouseDown);
					_ptMouseDownWindowPos.X = this.Left;
					_ptMouseDownWindowPos.Y = this.Top;
					_bMoving = true;
				};

				MouseMove += (sender, e) =>
				{
					if (_bMoving)
					{
						Point tmp = new Point(e.X, e.Y);
						tmp = PointToScreen(tmp);

						this.Left = _ptMouseDownWindowPos.X - _ptMouseDown.X + tmp.X;
						this.Top = _ptMouseDownWindowPos.Y - _ptMouseDown.Y + tmp.Y;
					}
				};

				MouseUp += (sender, e) =>
				{
					_bMoving = false;
					_ptMouseDown.X = -1;
					_ptMouseDown.Y = -1;
				};
			}


			ShowInTaskbar = false;		//タスクバーから消す		//追加


			//以下を追加
			//タスクトレイアイコン右クリックメニューの設定
			{
				ContextMenuStrip menu = new ContextMenuStrip();

				ToolStripItem[] stripItem = new ToolStripMenuItem[]
				{
					new ToolStripMenuItem("表示/最小化", null, delegate
						{
							if (WindowState == FormWindowState.Normal)
								WindowState = FormWindowState.Minimized;
							else if (WindowState == FormWindowState.Minimized)
								WindowState = FormWindowState.Normal;
						}),
					new ToolStripMenuItem("終了", null, delegate
						{
							Close();
						}),
				};
				menu.Items.AddRange(stripItem);

				notifyIcon1.ContextMenuStrip = menu;
			}

			//以下を追加
			//タスクトレイアイコンのダブルクリック処理
			notifyIcon1.DoubleClick += delegate
			{
				if (WindowState == FormWindowState.Normal)
					WindowState = FormWindowState.Minimized;
				else if (WindowState == FormWindowState.Minimized)
					WindowState = FormWindowState.Normal;
			};


			//フォームが表示されたときの処理
			Shown += delegate
			{
				//シークバーにフォーカスがあると、再生位置が表示されないのでここで再生ボタンにフォーカスを与える
				buttonPlay.Focus();
			};

プロジェクトファイルをダウンロード

前の10件 1  2  3  4  5  6  7  8  9  10  11