This tutorial include 3 parts,
- Archive Operations
- File List, Directory Tree and Drag n Drop
- Threading support (This Article)
Craft Your Own Archiver 3
Windows application generate a thread called main thread when started,
in the previous cyoa1 and cyoa2 demo, both UI and Archive operations is
run in the main thread. As a thread can only run one execution sequence
at a time, if the main thread is occupied by the archive operations
(which usually takes a long time), it will be unable to handle the UI
work, this will make the screen not updating (and a [Not responding]
appear in caption bar)
To avoid this problem, cakdir3 has a number of events, so when an event
is called, it will call each event handler one by one, and they update
the UI elements, and then when all event handler is called, it continue
to do other work. This improve the responsiveness of the application,
but the application is still laggy when an archive operation is working.
The another solution is to spawn a new thread for the archive
operations, since UI and archive operations run on different threads,
the UI is not affected.
To Do Something in a Thread
1) private void DoWork(object data)
2) {
.....
3) public void ThreadWork(object data)
4) {
5) Thread newThread = new Thread(new ParameterizedThreadStart(DoWork));
6) newThread.Start(data);
7) }
- Line 6, when called, will start
DoWork()
method in a new thread,
this will not block the current thread, unless you call
newThread.Join();
as the main thread and the newly created thread are two different
threads, one should not call the UI elements from the new Thread
directly, and should use event instead.
0.9) public EventHandler OnFinished;
1) private void DoWork(object data)
2.1) {
2.2)
2.3) if (OnFinished != null)
2.4) OnFinished(this, new EventArgs());
2.5) }
...
3) public void ThreadWork(object data)
4) {
4.1) OnFinished += new EventHandler(OnThreadFinished);
5) Thread newThread = new Thread(new ParameterizedThreadStart(DoWork));
6) newThread.Start(data);
7) }
...
8) public void OnThreadFinished(object sender, EventArgs e)
9) {
0) if (tbMessage.InvokeRequired)
a) tbMessage.Invoke(new EventHandler(OnThreadFinished), new object[] { sender, e });
b) else tbMessage.Text += "Finished" + Environment.NewLine;
c) }
- Line 0-b, the
InvokeRequired
return whether the current thread (the thread running the code) is not equal to the control's thread (the UI thread).
If true, it will call the same event again using the control's thread,
and InvokeRequired
will be called again and it will return false.
if false, it will do the UI work.
This is how to thread the work, but Cake3 have a lot of events,
operations, it will be time consuming to write threading support, thus
I wrote a component for that.
Cake3 Thread Support Class (namespace Cake3.Queue)
CakdirWorkItem
: represent one Extract, Add or Delete operations, support threading itself.
CakdirThreadQueue
- A first-in, first-out queue for CakdirWorkItem
.
ThreadQueueMonitor
- WinForms control for viewing the CakdirThreadQueue
.
To use CakdirWorkItem
1) private CakdirWorkitem workItem;
...
2) ExtractOptions extrOptions = new ExtractOptions(archiveName, extractTo, new string[] { "*" }, allowOverwrite, useFolder);
3) workItem = new CakdirWorkItem(extrOptions);
4) workItem.OnStopWorking += new EventHandler(WorkItem_StopWorking);
5) workItem.Start();
- Line 2,
ExtractOptions
is actually the same as cakdir.ExtractOptions
, you can pass cakdir.ExtractOptions
instead. - Line 3,
CakdirWorkItem
can be creating using a ExtractOptions
, AddOptions
or DeleteOptions
as parameter.
- Line 4,
workItem
has all the cakdir
events (e.g. startworking, stopworking, progress) remember you have to use the InvokeRequired
mentioned above when handling events.
- Line 5,
workItem.Start()
will run the operation in a new thread, use workItem.Run()
if you want to run in the same thread.
To use CakdirThreadQueue: (CakdirThreadQueue is not used in the demo)
1) public CakdirThreadQueue CTQ;
...
2) CTQ = new CakdirThreadQueue();
3) CTQ.OnQueueChanged = new EventHandler(CTQ_QueueChanged);
...
4) CTQ.Enqueue(workItem);
...
5) public void CTQ_QueueChanged(object sender, EventArgs e)
6) {
7) if (this.InvokeRequired)
8) this.Invoke(new EventHandler(CTQ_QueueChanged), new object[] {sender, e});
9) else
0) if (CTQ.Count == 0)
a) tbMessage.Text += "Finished";
b) else tbMessage.Text += CTQ.Queue[0].LastMessage;
c) }
- Line 3,
QueueChanged
is called everytime a workItem added, started, stopped, removed. - Line
4, will add the workitem to the queue, remember that you dont have to
call the
Start()
method, the queue will start it automatically when it
is available.
To Use ThreadQueueMonitor
Drop it to your form (queueMonitor), then call the following to link the CakdirThreadQueue
.
1) queueMonitor.RegisterThreadQueue(CTQ);
You may want to
write your own ThreadQueueMonitor based on Cakdir's one as well, source
code can be found under "Cake3 \ Queue \ ThreadQueueMonitor.cs".
How - tos
Get Path Information
string Current = Directory.GetCurrentDirectory();
string Desktop = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
string MyDocument = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
string MyApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
string System = Environment.GetFolderPath(Environment.SpecialFolder.System);
string Framework = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();
string Temp = System.IO.Path.GetTempPath();
string CurrentUserPath { get { RegistryKey rKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList", false);
string dir = Utils.AppendSlash((string)rKey.GetValue("ProfilesDirectory"));
string userdir = dir + System.Environment.UserName + "\\";
if (Directory.Exists(userdir)) return userdir;
if (Directory.Exists(dir)) return dir;
return (new DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.Personal)).Parent.FullName); } }
string SharedPath { get { RegistryKey rKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList", false);
string dir = (string)rKey.GetValue("Public");
if (Utils.DirectoryExists(dir)) return dir;
return Path.Combine(new DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.Personal)).Parent.Parent.FullName, "Public"); } }
string ProgramPath { get { System.Reflection.Assembly assembly;
assembly = System.Reflection.Assembly.GetExecutingAssembly();
if (assembly != null)
return Path.GetPathRoot(assembly.Location);
else
return Path.GetPathRoot(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName)); } }
Rename a file or folder in cakdir
private static void RenameFile(Cakdir3 cakdir, string fullPath, string newPath)
{
string tempPath = Utils.NewTempPath("qzTemp");
string basePath = Utils.AppendSlash(tempPath);
cakdir.Extract(fullPath, tempPath, true, true);
fullPath = Utils.RemoveFrontSlash(fullPath);
newPath = Utils.RemoveFrontSlash(newPath);
File.Copy(tempPath + fullPath, tempPath + newPath);
cakdir.AddOptions.baseFolder = basePath;
cakdir.AddOptions.addFile = new string[] { basePath + newPath };
cakdir.AddOptions.addFolder = AddOptions.folderMode.relative;
cakdir.Add();
cakdir.Delete(fullPath);
cakdir.List("*");
}
private static void RenameFolder(Cakdir3 cakdir, string fullPath, string newPath)
{
string path = Utils.AppendSlash(fullPath);
string lastPath = Utils.ExtractFileName(Utils.RemoveSlash(path));
if (lastPath == newPath)
return;
string tempPath = Utils.NewTempPath("qzTemp");
string basePath = Utils.AppendSlash(tempPath + Utils.RemoveFrontSlash(path));
cakdir.Extract(path + "*", tempPath, true, true);
cakdir.Delete(path + "*");
cakdir.AddOptions.baseFolder = basePath;
cakdir.AddOptions.addFile = new string[] { basePath + "*" };
cakdir.AddOptions.addFolder = AddOptions.folderMode.relative;
cakdir.AddToFolder(path.Replace("\\" + lastPath, "") + newPath);
cakdir.List("*");
}
HotEdit
(Allow user to modify a file/path in archive then update, actually just extract, display a dialog and then add.)
internal static void HotEdit(string archive, string path, string hotEditPath)
{
string tempPath;
if (hotEditPath != "")
{
Utils.RemoveFile(hotEditPath + path);
tempPath = hotEditPath;
}
else tempPath = Utils.NewTempPath("qzTemp");
Cakdir3 c3 = new Cakdir3(archive);
c3.Extract(path, tempPath, true, true);
path = Utils.RemoveFrontSlash(path);
Utils.OpenDirectory(Utils.ExtractFilePath(tempPath + path));
string msg = String.Format(
"HotEdit started, any changes to the following file will be updated to the archive when you pressed [OK]. \r\n" +
"\r\n"+
"Archive : {0}\r\n" +
"File : {1}\r\n\r\n" +
"Press [OK] to stop monitoring.", archive, tempPath + path);
MessageBox.Show(msg, archive, MessageBoxButtons.OK, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button1);
c3.Add(tempPath + path, AddOptions.folderMode.relative, tempPath);
}
Read and write INI file : (Download qzIniFiles__Cs_.zip - 2.45 KB )
IniFile = new QzIniFiles(iniFile);
IniFile["Section"]["Key"].Value = "aString";
IniFile["Section"]["Key1"].ValueAsInt = 1;
IniFile["Section"]["Key2"].ValueAsBool = true;
Debug.WriteLine(IniFile["Section"]["Key"].Value);
Debug.WriteLine(IniFile["Section"]["Key1"].ValueAsInt);
Debug.WriteLine(IniFile["Section"]["Key2"].ValueAsBool);
ArrayList strings = new ArrayList();
strings.Add("1");
strings.Add("2");
strings.Add("3");
IniFile.WriteStringList("Section", "List", strings);
ArrayList strings2 = IniFile.ReadStringList("Section","List"));
IniFile.UpdateFile();
Create Shortcut
check here
Get Context Menu Commands for Any File
internal static Hashtable PollShCmdList(string filename)
{
Hashtable retVal = new Hashtable();
if (filename != "")
{
string ext = Utils.ExtractFileExt(filename);
if (ext == "") return retVal;
RegistryKey rk0 = Registry.ClassesRoot.OpenSubKey(ext, false);
if (rk0 != null)
{
string ftype = (string)rk0.GetValue("");
rk0.Close();
if (ftype != null)
{
RegistryKey rk = Registry.ClassesRoot.OpenSubKey(ftype + "\\shell\\", false);
if (rk != null)
{
string[] keys = rk.GetSubKeyNames();;
rk.Close();
foreach (string key in keys)
if ((key.ToLower() != "printto") &&
(key.ToLower() != "runas") &&
(key.ToLower() != "file"))
{
RegistryKey rk1 = Registry.ClassesRoot.OpenSubKey(ftype + "\\shell\\" + key + "\\command", false);
string cmdline = (string)rk1.GetValue("");
rk1.Close();
if (cmdline != "")
retVal.Add(key, cmdline);
}
}
}
}
if (retVal.Count == 0)
retVal.Add("Open with...", "");
}
return retVal;
}
Get Icon for Non-Existing File
[StructLayout(LayoutKind.Sequential)]
internal struct SHFILEINFO
{
public IntPtr hIcon;
public IntPtr iIcon;
public uint dwAttributes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szDisplayName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
public string szTypeName;
};
internal const uint SHGFI_ICON = 0x100;
internal const uint SHGFI_TYPENAME =0x400;
internal const uint SHGFI_LARGEICON = 0x0;
internal const uint SHGFI_SMALLICON = 0x1;
internal const uint SHGFI_SYSICONINDEX = 16384;
internal const uint SHGFI_USEFILEATTRIBUTES = 16;
[DllImport("shell32.dll")]
internal static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes,
ref SHFILEINFO psfi, uint cbSizeFileInfo, uint uFlags);
public static Icon GetSmallFileIcon(string fileName)
{
SHFILEINFO shinfo = new SHFILEINFO();
SHGetFileInfo(fileName, 0, ref shinfo,
(uint)Marshal.SizeOf(shinfo),SHGFI_ICON |SHGFI_SMALLICON |
SHGFI_SYSICONINDEX | SHGFI_USEFILEATTRIBUTES);
return System.Drawing.Icon.FromHandle(shinfo.hIcon);
}
public static Icon GetLargeFileIcon(string fileName)
{
SHFILEINFO shinfo = new SHFILEINFO();
SHGetFileInfo(fileName, 0, ref shinfo,
(uint)Marshal.SizeOf(shinfo),SHGFI_ICON |
SHGFI_SYSICONINDEX | SHGFI_USEFILEATTRIBUTES);
return System.Drawing.Icon.FromHandle(shinfo.hIcon);
}
Further Reading
History
- 12-05-2008 - First submitted to CodeProject.
- 12-14-2008 - Add QzIniFiles, How-tos