Find duplicate files in C#, Part 4 of 4


[duplicate files]

The last three posts described an application that searches for duplicate files and removes them. The program seems to work fairly well, at least for small test directories. When I tried it on a directory containing around 8,000 files, however, it took around 229 seconds (3 minutes 49 seconds). That may not be the end of the world because you probably won’t need to check a particular directory for duplicates very often, but it still seemed like a long time.

The reason the program takes so long is that it computes a hash code for each file. To compute a hash code, the cryptographic methods must open the file, read it completely, and produce a code. Processing each file in its entirety takes a while.

Another test you can perform to see if two files are the same is to compare their file sizes. This is much less reliable than a hash code because many files might just happen to have the same sizes without being the same. However, comparing file sizes is much faster than hashing two files and comparing their hash codes. If two files have different sizes, then they are definitely not the same. If they have the same sizes, they might still be different. In that case, you can use the slower hash code to see if they really are the same.

This version of the program starts by grouping files by size. If a group contains a single file, then you know there is no duplicate for that file so you can eliminate it from consideration. After eliminating those files, the program uses the previous method of comparing hash codes to see which of the remaining files are duplicates.

The following code shows the LINQ queries that this version uses to identify duplicates.

// Get FileInfos representing the files.
var get_infos =
    from string filename in Directory.GetFiles(txtDirectory.Text)
    select new FileInfo(filename);

// Group the FileInfos by file length.
var fileinfo_groups =
    from FileInfo file_info in get_infos
    group file_info by file_info.Length into g
    where g.Count() > 1
    select g;

// Flatten the result to get a list of FileInfos.
var flattened = fileinfo_groups.SelectMany(x => x).ToList();

// Get a list of the files and their hash values.
var get_info =
    from FileInfo file_info in flattened
    select new
    {
        Name = file_info.FullName,
        Hash = GetHashString(file_info.FullName)
    };

// Group the files by hash value.
var group_infos =
    from info in get_info
    group info by info.Hash into g
    where g.Count() > 1
    //orderby g.Key
    select g;

// Loop through the files.
int num_groups = 0;
int num_files = 0;
foreach (var g in group_infos)
{
   ...
}

The get_infos query loops through all of the files in the directory and selects FileInfo objects representing the files.

Next, the fileinfo_groups query groups the FileInfo objects by file length. It keeps only the groups that contain more than one file. (The groups that might contain a duplicate.)

The code then uses the previous query’s SelectMany method to select the items in the groups built by the fileinfo_groups query. This combines the values in the groups to make a single flattened group containing all of the selected FileInfo objects. After using the SelectMany method, the program uses ToList to convert the result into a list.

Now the program returns to something similar to the previous version. The get_info query loops through the remaining files and selects their names and hash codes.

Finally, the group_infos query groups the files by hash code and selects only the groups that contain more than one file.

After this point, the code populates the TreeView control as in the previous examples. See the previous posts for details.

With this change, the program processes the 8,000 file directory in about 61 seconds, much faster than the previous version’s 229 seconds!

Note that some of the information that the two versions collect seems to be cached. Perhaps the disk drive is storing some of the file information it gathered in memory. This means if you run the program repeatedly, the run times may vary greatly.

In any case, the new version is fast enough to be useful for searching moderately large directories. Click the Download button to get the new version of the program.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, cryptography, files, system, tools | Tagged , , , , , , , , , , , , , , , , , , | 1 Comment

Find duplicate files in C#, Part 3 of 4


[duplicate files]

The last two posts described an application that searches for duplicate files and removes them. This post explains how the program removes the files when you click the Delete Selected button. When you click the button, the following code executes.

// Delete the selected files.
private bool Deleting = false;
private void btnDeleteSelected_Click(object sender, EventArgs e)
{
    Deleting = true;
    trvFiles.Visible = false;
    lblNumDuplicates.Text = "";
    picImage.Visible = false;
    picImage.Image = null;
    rchText.Visible = false;
    Cursor = Cursors.WaitCursor;
    Refresh();

    try
    {
        // Make a list of nodes to delete.
        List<TreeNode> nodes_to_delete = new List<TreeNode>();
        foreach (TreeNode hash_node in trvFiles.Nodes)
        {
            foreach (TreeNode file_node in hash_node.Nodes)
            {
                // See if the file's node is checked.
                if (file_node.Checked)
                    nodes_to_delete.Add(file_node);
            }
        }

        // Delete the selected nodes and their files.
        foreach (TreeNode file_node in nodes_to_delete)
        {
            // Get the FileInfo from the Tag property.
            FileInfo file_info = file_node.Tag as FileInfo;

            // Move the file into the recycle bin.
            DeleteFile(file_info.FullName);

            // Remove the file from the TreeView.
            file_node.Remove();
        }

        // Make a list of size nodes with no remaining children.
        nodes_to_delete = new List<TreeNode>();
        foreach (TreeNode hash_node in trvFiles.Nodes)
        {
            if (hash_node.Nodes.Count == 0)
                nodes_to_delete.Add(hash_node);
        }

        // Delete the size nodes with no remaining children.
        foreach (TreeNode hash_node in nodes_to_delete)
        {
            hash_node.Remove();
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
    finally
    {
        // Scroll to the top.
        if (trvFiles.Nodes.Count > 0)
            trvFiles.Nodes[0].EnsureVisible();
        trvFiles.Visible = true;

        Deleting = false;
        Cursor = Cursors.Default;
    }
}

If the code deleted a TreeView node that happens to be selected, the TreeView control selects a new node. That makes the program display the selected file’s contents. The whole process slows things down and can be really silly with the program displaying a bunch of files as quickly as it can.

To prevent that, the program defines the variable Deleting. When this value is true, the program doesn’t display the selected file. (See the previous post to see how that part works.)

To get ready, the btnDeleteSelected_Click event handler sets Deleting to true. It also hides the picImage and rchText controls that the program uses to display files, and it hides the TreeView control so the control doesn’t need to update itself every time a node is deleted.

Next, the code loops through the top-level TreeView nodes. For each top-level node, the code loops through its child nodes. If a child node is checked, the program adds it to the nodes_to_delete list.

After examining all of the tree’s nodes, the program loops through the list of nodes to delete. For each node, it gets the FileInfo object stored in the node’s Tag property. It calls the DeleteFile method to move the file into the recycle bin and then removes the node from the TreeView control.

(For information on the DeleteFile method, see my post Manage the recycle bin (wastebasket) in C#.)

After deleting the selected file nodes, some of the top-level nodes may have no remaining children so they should be removed. The program loops through the top-level nodes and builds a list of those with no children. It then loops through the list and removes those nodes from the TreeView control. (You could probably also remove top-level nodes that only have one child.)

After is has finished deleting the appropriate nodes, the program scrolls to the top and makes the TreeView visible.

That finishes the explanation of the program’s main features. Download the example program to see additional details.

In the next and final post in this series, I’ll explain a big performance improvement.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, cryptography, files, system, tools | Tagged , , , , , , , , , , , , , , , , , , | Leave a comment

Find duplicate files in C#, Part 2 of 4


[duplicate files]

The previous post Find duplicate files in C#, Part 1 of 4 explained how the example uses a LINQ query to select files grouped by hash code. This post explains how the program displays the files’ contents and how the Select Duplicate button works.

When you click on a node in the TreeView control, the following AfterSelect event handler executes.

// Display the clicked file.
private void trvFiles_AfterSelect(object sender, TreeViewEventArgs e)
{
    if (Deleting) return;

    // Hide the display controls.
    rchText.Visible = false;
    picImage.Visible = false;
    Refresh();

    // Do nothing for size nodes.
    if (e.Node.Level == 0) return;

    // Get the file's information.
    FileInfo file_info = e.Node.Tag as FileInfo;
    switch (file_info.Extension.ToLower())
    {
        case ".txt":    // Text files.
        case ".html":
        case ".cs":
        case ".csproj":
        case ".resx":
        case ".xml":
        case ".xaml":
        case ".config":
            rchText.Text = File.ReadAllText(file_info.FullName);
            rchText.Visible = true;
            break;

        case ".rtf":    // Rich text.
            rchText.LoadFile(file_info.FullName);
            rchText.Visible = true;
            break;

        case ".jpg":    // Image files.
        case ".jpeg":
        case ".gif":
        case ".png":
        case ".tiff":
        case ".bmp":
            picImage.Image = LoadBitmapUnlocked(file_info.FullName);
            picImage.Visible = true;
            break;

        default:
            rchText.Text = "Unknown file extension " +
                file_info.Extension;
            rchText.Visible = true;
            break;
    }
}

The event handler executes sometimes while the program is manipulating the TreeView control. To prevent it from displaying unwanted file previews, the event handler checks the Deleting variable and exits if Deleting is true.

The code then hides the rchText and picImage controls that it will use to display file previews.

If the level of the node selected is 0, then this is a top-level hash code node. No file was selected so there’s nothing to show and the event handler exits.

If this is a file’s node, the program gets the FileInfo object stored in the node’s Tag property (see the previous post). It checks the FileInfo object’s Extension property and takes action depending on what kind of file it represents.

If this is a text file, the code uses File.ReadAllText to get the file’s contents and then displays them as text in the RichTextBox named rchText.

If this is a Rich Text file, the code uses the rchText control’s LoadFile method to load the file into the RichTextBox.

If this is a graphics file, the code uses the LoadBitmapUnlocked method to load the file without locking it. To see how that method works, download the example and see the post Load images without locking their files in C#.


When you click the Select Duplicates button, the following code executes.

// Select all but the first file in each group.
private void btnSelectDuplicates_Click(object sender, EventArgs e)
{
    foreach (TreeNode hash_node in trvFiles.Nodes)
    {
        hash_node.Checked = false;
        hash_node.Nodes[0].Checked = false;
        for (int i = 1; i < hash_node.Nodes.Count; i++)
            hash_node.Nodes[i].Checked = true;
    }
}

This code loops through the TreeView control’s Nodes collection. That collection only lists the control’s top-level nodes so the loop only covers them.

The loop unchecks the top-level nodes and the first child nodes of the top-level nodes. It then loops through the top-level node’s other child nodes (that’s why the loop starts with index 1), checking them. When the code is finished, the top-level nodes and one child each are unchecked. The rest of the nodes are checked.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, cryptography, files, system, tools | Tagged , , , , , , , , , , , , , , , , , , | 1 Comment

Find duplicate files in C#, Part 1 of 4


[duplicate files]

This example lets you find and remove duplicate files. It’s fairly complex, so I’m going to cover it’s more interesting pieces in several posts. I won’t cover some of the less interesting pieces at all.

Enter a directory path or click the ellipsis button to browse for a folder. When you click Search, the program searches for duplicate files in that directory and displays them in the TreeView control on the left. It groups the files by hash code.

If two files have the same hash code, then they are very likely to be the same, but there is a tiny chance (roughly 1 in 65,000 for this hash algorithm) that the two files are different. If you click on a file, the program displays it on the right. (The program can display several kinds of text and graphic files.) By clicking on the files, you can determine whether they are actually different.

If you click the Select Duplicates button, the program checks all of the files except the first one in each hash group. You can also check and uncheck the files individually by hand.

If you click the Delete Selected button, the program moves the files you selected into the recycle bin.

This post explains how the program finds the duplicated files. When you click the Search button, the following code executes.

// Search the directory for duplicates.
private void btnSearch_Click(object sender, EventArgs e)
{
    Stopwatch watch = new Stopwatch();
    watch.Start();

    Cursor = Cursors.WaitCursor;
    trvFiles.Visible = false;
    trvFiles.Nodes.Clear();
    lblNumDuplicates.Text = "";
    Refresh();

    try
    {
        // Get a list of the files and their hash values.
        var get_info =
            from string filename in
                Directory.GetFiles(txtDirectory.Text)
            select new
            {
                Name = filename,
                Hash = BytesToString(GetHash(filename))
            };

        // Group the files by hash value.
        var group_infos =
            from info in get_info
            group info by info.Hash into g
            where g.Count() > 1
            //orderby g.Key
            select g;

        // Loop through the files.
        int num_groups = 0;
        int num_files = 0;
        foreach (var g in group_infos)
        {
            num_groups++;
            TreeNode hash_node = trvFiles.Nodes.Add(
                g.Key.ToString());
            foreach (var info in g)
            {
                num_files++;
                TreeNode file_node = new TreeNode(info.Name);
                file_node.Tag = new FileInfo(info.Name);
                hash_node.Nodes.Add(file_node);
            }
        }

        // Display the number of duplicates.
        lblNumDuplicates.Text =
            (num_files - num_groups).ToString() +
            " duplicate files";

        // Expand all nodes.
        trvFiles.ExpandAll();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
    finally
    {
        // Scroll to the top.
        if (trvFiles.Nodes.Count > 0)
            trvFiles.Nodes[0].EnsureVisible();
        trvFiles.Visible = true;

        Cursor = Cursors.Default;
    }

    watch.Stop();
    Console.WriteLine(watch.Elapsed.TotalSeconds.ToString("0.00") +
        " seconds");
}

The code performs some initialization tasks such as creating a Stopwatch and displaying the wait cursor.

Next, the code creates a LINQ query that loops over the files returned by the Directory.GetFiles method, which returns the names of the files in a directory. For each file, the query selects the file’s name and the file’s MD5 hash code. (See the example Calculate hash codes for a file in C# for information on how to get the hash code and convert it into a string.)

The code then uses another LINQ query to group the results of the first query by hash code. It selects only groups where the number of files in the group (i.e. have the same hash code) is greater than one. The result is a query that returns groups containing at least two files with the same hash codes.

(You can uncomment the orderby clause if you want to sort the groups by hash code. That slows the program down, though, and the hash codes don’t really mean anything so sorting them isn’t very useful. I put that in there so I could compare the results to the results of another version of the program that I’ll describe in a later post.)

Having built the grouping query, the program loops through the groups. For each group, the program adds a top-level node to the TreeView control displaying the hash code. It then loops through the group’s contents. Each item within the group contains a file’s name and hash code. The code creates a child node for the file, setting its text equal to the file’s name. It also sets the child node’s Tag property to a FileInfo object representing the file. Later it can use that object to display or delete the file.

As it processes the groups, the code keeps track of the number of duplicate files and the number of groups. It then reports [# files] – [# groups] as the number of duplicate files. (It basically assumes that one file in each group should be kept and the rest are duplicates.)

Next, the code expands all of the TreeView control’s nodes so you can see all of the duplicate files. It also scrolls to the top of the TreeView control. The code finishes by displaying the elapsed time in the Console window.

In the next few posts, I’ll explain how the program:

  • Displays file previews and selects the duplicate files when you click the Select Duplicates button
  • Deletes files when you click the Delete Selected button

See the following examples for more information on specific techniques that the program uses.

And download the example for other details.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, cryptography, files, system, tools | Tagged , , , , , , , , , , , , , , , , , , | 2 Comments

Write a CSV file from an array in C#

[CSV file]

The example Read a CSV file into an array in C# shows how to load a CSV file into a two-dimensional array of strings. This example does the opposite: it saves a two-dimensional array of strings into a CSV (comma-separated value) file.

When you click the program’s Write button, the following code builds a two-dimensional array of data and uses it to write a CSV file.

// Write a CSV file.
private void btnWrite_Click(object sender, EventArgs e)
{
    // Create an array of data.
    // Note: The data must not contain commas.
    string[,] values =
    {
        { "WPF 3d-Three-Dimensional Graphics with WPF and C#", ... }, 
        { "The C# Helper Top 100-The 100 most popular posts at csharphelper.com", ... }, 
        { "Interview Puzzles Dissected-Solving and Understanding Interview Puzzles", ... }, 
        ...
    };

    // Convert the array into a CSV string.
    string csv = ArrayToCsv(values);

    // Write the CSV file.
    File.WriteAllText(txtFile.Text, csv);

    // Display the result.
    txtCsv.Text = File.ReadAllText(txtFile.Text);
}

This code first creates a two-dimensional array of strings. The values are wired into the code, but you could generate the values in whatever way suits your needs.

The code then calls the ArrayToCsv method (described shortly) to convert the array into a string with CSV format. It then simply calls File.WriteAllText to write that string into a CSV file. The code finishes by displaying the file’s contents in a text box so you can see what it looks like.

The following code shows how the ArrayToCsv method converts a two-dimensional array of string values into a single string with CSV format.

// Convert array data into CSV format.
private string ArrayToCsv(string[,] values)
{
    // Get the bounds.
    int num_rows = values.GetUpperBound(0) + 1;
    int num_cols = values.GetUpperBound(1) + 1;

    // Convert the array into a CSV string.
    StringBuilder sb = new StringBuilder();
    for (int row = 0; row < num_rows; row++)
    {
        // Add the first field in this row.
        sb.Append(values[row, 0]);

        // Add the other fields in this row separated by commas.
        for (int col = 1; col < num_cols; col++)
            sb.Append("," + values[row, col]);

        // Move to the next line.
        sb.AppendLine();
    }

    // Return the CSV format string.
    return sb.ToString();
}

The method first gets the array’s dimensions and then creates a StringBuilder to hold the final CSV string.

Next, the code loops through the array’s rows. For each row, it adds the row’s first value to the StringBuilder. It then loops through the rest of the fields in that row and adds them to the StringBuilder, separating them from the previous fields by commas.

After it has added all of a row’s fields to the StringBuilder, the code adds a new line.

After it has processed all of the array’s rows, the code converts the StringBuilder into a string and returns the result.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in database, files, strings | Tagged , , , , , , , , , , , , , , | Leave a comment

Calculate hash codes for a file in C#

[hash codes]

Hash codes are codes that concisely represent files or other chunks of data. The basic idea is to combine the data’s bytes in ways so that two different files are likely to have different hash codes. If you save a file’s hash code, you can tell whether someone has modified the file by calculating a new hash code and seeing if it matches the value you originally saved.

This example, uses two different hashing algorithms: MD5 and SHA256. Both are provided by the System.Security.Cryptography namespace so the program includes the following using directive.

using System.Security.Cryptography;

The following code shows how the program calculates a file’s SHA256 hash code.

// The cryptographic service provider.
private SHA256 Sha256 = SHA256.Create();

// Compute the file's hash.
private byte[] GetHashSha256(string filename)
{
    using (FileStream stream = File.OpenRead(filename))
    {
        return Sha256.ComputeHash(stream);
    }
}

The program declares an SHA256 object at the class level. (You could create this object inside the GetHashSha256 method, but this version creates it at the class level so it only needs to do it once even if you calculate hash codes for many files.)

The GetHashSha256 method calculates the file’s hash code. It opens the file for reading and creates a FileStream associated with it. It then passes the stream to the SHA256 object’s ComputeHash method and returns the result.

The code to get hash codes using the HD5 algorithm is exactly the same as the code for the SHA256 algorithm. Just replace “SHA256” with “HD5” in the previous code.

The ComputeHash method returns a byte stream. The program can’t display a byte stream directly to the user, so it uses the following method to convert the bytes into a string.

// Return a byte array as a sequence of hex values.
public static string BytesToString(byte[] bytes)
{
    string result = "";
    foreach (byte b in bytes) result += b.ToString("x2");
    return result;
}

This code loops through the array’s bytes and uses ToString to convert each byte into a two-digit hexadecimal value. It concatenates the values and returns the result.

The rest is easy. The following code shows how the program displays the selected file’s hash codes.

// Compute the file's hash code.
private void btnHash_Click(object sender, EventArgs e)
{
    txtMd5.Text = BytesToString(GetHashMD5(txtFile.Text));
    txtSha256.Text = BytesToString(GetHashSha256(txtFile.Text));
}

This code simply calls the GetHashMD5 and GetHashSHA256 methods, passes the results to BytesToString to convert the hash codes into strings, and displays the results in read-only text boxes.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, cryptography, files | Tagged , , , , , , , , , , , , | 1 Comment

Copy a directory hierarchy into the executable directory in C#


[executable directory]

The example Copy files into the executable directory in C# explains how to make Visual Studio copy a file into the executable directory when it builds a project. That makes it easy for the executable to find the file when it runs.

This example explains how to copy a whole directory hierarchy into the executable directory. For this example, suppose the directory is named Images and it is in the project directory. (Where the source code is.) To make Visual Studio copy it, follow these steps:

  1. Select the Project menu’s Properties command to open the project’s property pages.
  2. On the Build Events page, click Edit Post-build.
  3. In the “Post-build event command line” text box, enter the following code and click OK.
    xcopy "$(ProjectDir)\Images" "Images" /s /i /y



When you successfully build the program, Visual Studio copies the directory hierarchy from $(ProjectDir)\Images (the project directory) to the Images directory inside the executable directory.

When it starts, the program uses the following code to display an image file that was copied with the Images directory.

// Load the form's background image from
// a file in the Images directory.
private void Form1_Load(object sender, EventArgs e)
{
    this.BackgroundImage =
        new Bitmap(@"Images\CapeRoyal_NorthRim_GrandCanyon.jpg");
    this.ClientSize = this.BackgroundImage.Size;
}

(You might like to spend a few moments experimenting with post-build macros. Click the “Edit Post-build” button to display the Post-build Event Command Line editor. Use that dialog’s Macro button to see what macros are available.)


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in files, miscellany, system | Tagged , , , , , , , , , , , , , , , | Leave a comment

Use an animated cursor in C#

[animated cursor]

This example doesn’t show how to use an animated cursor file such as an animated gif or png file. As far as I know, C# doesn’t support that kind of cursor.

What this example does do is show how to use a series of cursor images to make an animated cursor at runtime.

The program starts by using the following code to define an array of Cursor objects.

// Cursors.
private Cursor[] Cursors;
private const int NumCursors = 18;

The form’s Load event handler creates the cursors at run time. That code is kind of long so if you’re not interested in seeing how the cursors are created, skip down past the following code.

private void Form1_Load(object sender, EventArgs e)
{
    // Geometry.
    const int cursor_wid = 32;
    const int cursor_hgt = 32;
    float cx = cursor_wid / 2f;
    float cy = cursor_hgt / 2f;
    float rx = cx * 0.9f;
    float ry = cx * 0.4f;
    RectangleF rect = new RectangleF(-rx, -ry, 2 * rx, 2 * ry);
    float radius = cx * 0.15f;

    // Make the transformations we will use.
    Matrix transform1 = new Matrix();
    transform1.Rotate(60f, MatrixOrder.Append);
    transform1.Translate(cx, cy, MatrixOrder.Append);
    Matrix transform2 = new Matrix();
    transform2.Rotate(-60f, MatrixOrder.Append);
    transform2.Translate(cx, cy, MatrixOrder.Append);
    Matrix transform3 = new Matrix();
    transform3.Translate(cx, cy, MatrixOrder.Append);

    // Make an orbital image.
    Bitmap orbital_bm = new Bitmap(cursor_wid, cursor_hgt);
    using (Graphics gr = Graphics.FromImage(orbital_bm))
    {
        // Use a transparent background.
        gr.SmoothingMode = SmoothingMode.AntiAlias;
        gr.Clear(Color.Transparent);

        // Draw the orbitals.
        gr.Transform = transform1;
        gr.DrawEllipse(Pens.Red, rect);

        gr.Transform = transform2;
        gr.DrawEllipse(Pens.Red, rect);

        gr.Transform = transform3;
        gr.DrawEllipse(Pens.Red, rect);

        // Draw the nucleus.
        gr.FillEllipse(Brushes.Black,
            -radius, -radius, 2 * radius, 2 * radius);
    }

    // Make the cursors.
    Cursors = new Cursor[NumCursors];
    double theta1 = 0;
    double dtheta1 = 2 * Math.PI / NumCursors;
    double theta2 = 0;
    double dtheta2 = 2 * Math.PI / NumCursors * 2;
    double theta3 = 0;
    double dtheta3 = 2 * Math.PI / NumCursors * 3;
    for (int i = 0; i < NumCursors; i++)
    {
        Bitmap cursor_bm = new Bitmap(cursor_wid, cursor_hgt);
        using (Graphics gr = Graphics.FromImage(cursor_bm))
        {
            // Copy the background orbitals.
            gr.SmoothingMode = SmoothingMode.AntiAlias;
            gr.DrawImage(orbital_bm, 0, 0);

            // Draw the electrons.
            gr.Transform = transform1;
            double x1 = rx * Math.Cos(theta1);
            double y1 = ry * Math.Sin(theta1);
            gr.FillEllipse(Brushes.Red,
                (int)(x1 - radius), (int)(y1 - radius),
                2 * radius, 2 * radius);
            theta1 += dtheta1;

            gr.Transform = transform2;
            double x2 = rx * Math.Cos(theta2);
            double y2 = ry * Math.Sin(theta2);
            gr.FillEllipse(Brushes.Green,
                (int)(x2 - radius), (int)(y2 - radius),
                2 * radius, 2 * radius);
            theta2 += dtheta2;

            gr.Transform = transform3;
            double x3 = rx * Math.Cos(theta3);
            double y3 = ry * Math.Sin(theta3);
            gr.FillEllipse(Brushes.Blue,
                (int)(x3 - radius), (int)(y3 - radius),
                2 * radius, 2 * radius);
            theta3 += dtheta3;
        }

        // Turn the bitmap into a cursor.
        Cursors[i] = new Cursor(cursor_bm.GetHicon());

        // Increment theta.
        theta1 += dtheta1;
    }
}

The Load event handler makes three transformations to rotate images 60°, -60°, and 0°. Each of the transformations also translates the result to center it on the cursor bitmap.

Next, the code makes a bitmap holding the orbits and nucleus of the atom. All of the cursors will share that background. The code makes the orbits by drawing an ellipse using each of the three transformations. It uses the final transformation to draw the nucleus centered in the cursor bitmap.

Now the code enters a loop to draw each of the animated cursor images. For each cursor, the code copies the orbit background image into a new bitmap. It then draws three electrons using the three transformations. After it draws each electron, the code updates that electron’s theta value, which indicates its position along its orbit. Each theta value is updated by a different amount, so the electrons move at different speeds.

After it has drawn a cursor’s image, the code passes its GetHicon value to the Cursor constructor to make the Cursor object.

The program uses a Timer to produce the animated cursor. The following code shows the Timer component’s Tick event.

// Display the next cursor.
private int CursorNumber = 0;
private void tmrSwitchCursor_Tick(object sender, EventArgs e)
{
    CursorNumber = (CursorNumber + 1) % NumCursors;
    this.Cursor = Cursors[CursorNumber];
}

This code simply cycles through the animated cursor images.

This example also lets you click and scribble to draw. I added that mostly to let you see that the cursors’ hotspot is at the center where the nucleus is. This part of the program isn’t hard but it’s also not central to the example so it isn’t described here. Download the example to see how it works.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics | Tagged , , , , , , , , , , , , | Leave a comment

Display database pictures in a ListView control in C#

[display database pictures]

This example shows how you can display database pictures in a ListView control. My previous post, Create ListView icons at run time in C#, explained how to make images at runtime to display in a ListView control. This example follows from that one. Simply get the images you want from the database and the use them to create the images you want to display in the ListView.

That begs the question, “How do you get the images from the database? The example Load images from an Access database in C# shows how to do that.

This is a fairly long example but the separate pieces aren’t too bad. The basic steps are:

  • Read book records from the database.
  • Get the image in each record.
  • Resize the image appropriately without distorting it and save the resized image in the appropriate ImageList control, using the book’s title as the images’ keys.
  • Create the ListView item, using the book’s title as the item’s key so the ListView control knows which images to use for the item.

The paragraphs that follow explain the details.

The following code shows the part of the form’s Load event handler that works with the database.

// Compose the database file name.
// This assumes it's in the executable's directory.
string db_name = Application.StartupPath +
    "\\books_with_images.mdb";

// Connect to the database
using (OleDbConnection conn =
    new OleDbConnection(
        "Provider=Microsoft.ACE.OLEDB.12.0;" +
        "Data Source=" + db_name + ";" +
        "Mode=Share Deny None"))
{
    // Get the book information.
    OleDbCommand cmd = new OleDbCommand(
        "SELECT Title, URL, ISBN, CoverUrl, " +
        "Pages, Year, CoverImage FROM Books ORDER BY Year DESC",
        conn);
    conn.Open();
    using (OleDbDataReader reader = cmd.ExecuteReader())
    {
        lvwBooks.Items.Clear();
        imlLargeIcons.Images.Clear();
        imlSmallIcons.Images.Clear();
        while (reader.Read())
        {
            // Make the images.
            if (!reader.IsDBNull(6))
            {
                // Get the image.
                Bitmap bm = BytesToImage((byte[])reader.GetValue(6));
                float source_aspect = bm.Width / (float)bm.Height;

                // Make the large image.
                AddImageToImageList(imlLargeIcons,
                    bm, reader[0].ToString(),
                    imlLargeIcons.ImageSize.Width,
                    imlLargeIcons.ImageSize.Height);

                // Make the small image.
                AddImageToImageList(imlSmallIcons,
                    bm, reader[0].ToString(),
                    imlLargeIcons.ImageSize.Width,
                    imlLargeIcons.ImageSize.Height);
            }

            // Add the data row.
            lvwBooks.AddRow(
                reader[0].ToString(),   // Image key
                reader[0].ToString(),   // Title
                reader[1].ToString(),   // URL
                reader[2].ToString(),   // ISBN
                reader[3].ToString(),   // CoverUrl
                reader[4].ToString(),   // Pages
                reader[5].ToString());  // Year
        }
    }
}

This code connects to an Access database. (You should modify that part to connect to your database if you’re not using Access or if you need to use a different provider.) It then executes a database query that selects data from the Books table. The CoverImage field holds the books’ cover images.

The code clears the ListView and ImageList controls, and then enters a while loop that runs as long as the query returns another row of data.

Inside the loop, the code uses the reader’s IsDBNull method to see whether the image is empty. (There’s no point in doing a lot of work building a bitmap if there’s no image in the database.)

If the image exists, the program uses BytesToImage method described shortly to convert the data in that field into a Bitmap. The code then calls the AddImageToImageList method (also described shortly) twice to make large and small versions of the cover image and to add them to the program’s ImageList controls. The images are saved in the ImageList controls with the book’s title used as a key.

Having created the images, the program calls the AddRow extension method to create the ListView row.

The following code shows the BytesToImage method.

// Convert a byte array into an image.
private Bitmap BytesToImage(byte[] bytes)
{
    using (MemoryStream image_stream =
        new MemoryStream(bytes))
    {
        Bitmap bm = new Bitmap(image_stream);
        return bm;
    }
}

This method creates a MemoryStream representing the bytes that hold the image data. It passes the Bitmap constructor the stream and returns the resulting Bitmap object.

The following code shows the AddImageToImageList method.

// Scale the image to fit in the ImageList and add it.
private void AddImageToImageList(ImageList iml, Bitmap bm,
    string key, float wid, float hgt)
{
    // Make the bitmap.
    Bitmap iml_bm = new Bitmap(
        iml.ImageSize.Width,
        iml.ImageSize.Height);
    using (Graphics gr = Graphics.FromImage(iml_bm))
    {
        gr.Clear(Color.Transparent);
        gr.InterpolationMode = InterpolationMode.High;

        // See where we need to draw the image to scale it properly.
        RectangleF source_rect = new RectangleF(
            0, 0, bm.Width, bm.Height);
        RectangleF dest_rect = new RectangleF(
            0, 0, iml_bm.Width, iml_bm.Height);
        dest_rect = ScaleRect(source_rect, dest_rect);

        // Draw the image.
        gr.DrawImage(bm, dest_rect, source_rect,
            GraphicsUnit.Pixel);
    }

    // Add the image to the ImageList.
    iml.Images.Add(key, iml_bm);
}

This method scales an image to make it as large as possible within specific dimensions and without distorting it. It then adds the image to an ImageList control. This program uses the method twice per image to add pictures to the small and large ImageList controls.

The method first creates a Bitmap that is the right size for the ImageList control. It then makes a Graphics object associated with the Bitmap.

The code clears the Bitmap with a transparent background. It then creates two rectangles. The rectangle source_rect represents the size of the input Bitmap that must be scaled. The rectangle dest_rect represents the size available in the new Bitmap. (The size of the images in the ImageList control.)

Next, the code calls the ScaleRect method to get the largest rectangle that fits in dest_rect without distortion. The code uses the resulting rectangle and source_rect to draw the input image into the new Bitmap. The method finishes by adding the new image to the ImageList control. It gives the new image the key value that was passed into the method.

The following code shows the ScaleRect method.

// Scale an image without disorting it.
// Return a centered rectangle in the destination area.
private RectangleF ScaleRect(
    RectangleF source_rect, RectangleF dest_rect)
{
    float source_aspect =
        source_rect.Width / source_rect.Height;
    float wid = dest_rect.Width;
    float hgt = dest_rect.Height;
    float dest_aspect = wid / hgt;

    if (source_aspect > dest_aspect)
    {
        // The source is relatively short and wide.
        // Use all of the available width.
        hgt = wid / source_aspect;
    }
    else
    {
        // The source is relatively tall and thin.
        // Use all of the available height.
        wid = hgt * source_aspect;
    }

    // Center it.
    float x = dest_rect.Left + (dest_rect.Width - wid) / 2;
    float y = dest_rect.Top + (dest_rect.Height - hgt) / 2;
    return new RectangleF(x, y, wid, hgt);
}

This method calculates the aspect ratios (ratio of width to height) of the two rectangles. If the source rectangle is relatively short and wide compared to the destination rectangle, the code makes the result as wide as the destination rectangle and calculates its height. If the source rectangle is relatively tall and thin compared to the destination rectangle, the code makes the result as tall as the destination rectangle and calculates its width.

The method centers the resulting rectangle and returns the result.

The final interesting piece of the program is the following AddRow extension method.

// Add a row to the ListView.
public static void AddRow(this ListView lvw, string key,
    string item_title, params string[] subitem_titles)
{
    // Make the item.
    ListViewItem new_item = lvw.Items.Add(item_title, key);

    // Make the sub-items.
    for (int i = subitem_titles.GetLowerBound(0);
             i <= subitem_titles.GetUpperBound(0);
             i++)
    {
        new_item.SubItems.Add(subitem_titles[i]);
    }
}

This method creates a new ListViewItem giving it the indicated text and key string. This key string lets the ListView figure out which image in the ImageList controls to use for the item. The method then loops through the item's other values to create the ListView sub-items.

Download the example to see additional details.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in controls, user interface | Tagged , , , , , , , , , , , , , , , , , , , | 9 Comments

Create ListView icons at run time in C#

[ListView icons]

The example Display large and small ListView icons in C# explains how to display ListView icons next to items. Basically, you place the images in two ImageList controls, set the ListView control’s LargeImageList and SmallImageList properties to those two controls, and set the items’ ImageIndex properties to the appropriate indices for the correct ListView icons.

In the previous example, most of that work was done at design time. This example does it at run time. The following code does most of the work.

private void Form1_Load(object sender, EventArgs e)
{
    // Select the first style.
    cboStyle.SelectedIndex = 0;

    // Initialize the ListView.
    lvwBooks.SmallImageList = imlSmallIcons;
    lvwBooks.LargeImageList = imlLargeIcons;

    // Make the column headers.
    lvwBooks.MakeColumnHeaders(
        "Title", 230, HorizontalAlignment.Left,
        "URL", 220, HorizontalAlignment.Left,
        "ISBN", 130, HorizontalAlignment.Left,
        "Picture", 230, HorizontalAlignment.Left,
        "Pages", 50, HorizontalAlignment.Right,
        "Year", 60, HorizontalAlignment.Right);

    // Create images.
    RectangleF small_rect = new RectangleF(0, 0, 32, 32);
    RectangleF large_rect = new RectangleF(3, 3, 58, 58);
    imlSmallIcons.Images.Clear();
    imlLargeIcons.Images.Clear();
    using (Pen small_pen = new Pen(Color.Blue, 2))
    {
        using (Pen large_pen = new Pen(Color.Blue, 3))
        {
            large_pen.LineJoin = LineJoin.Round;
            for (int i = 1; i <= 7; i++)
            {
                Bitmap bm32x32 = new Bitmap(32, 32);
                using (Graphics gr = Graphics.FromImage(bm32x32))
                {
                    gr.SmoothingMode = SmoothingMode.AntiAlias;
                    gr.Clear(Color.Transparent);
                    DrawStar(gr, small_rect, i + 3, small_pen,
                        Brushes.Yellow);
                    // Save the image using i as the image's key.
                    imlSmallIcons.Images.Add(i.ToString(), bm32x32);
                }

                Bitmap bm64x64 = new Bitmap(64, 64);
                using (Graphics gr = Graphics.FromImage(bm64x64))
                {
                    gr.Clear(Color.Transparent);
                    gr.SmoothingMode = SmoothingMode.AntiAlias;
                    DrawStar(gr, large_rect, i + 3, large_pen,
                        Brushes.Yellow);
                    // Save the image using i as the image's key.
                    imlLargeIcons.Images.Add(i.ToString(), bm64x64);
                }
            }
        }
    }

    // Add data rows.
    lvwBooks.AddRow("1", "WPF 3d", ...
    lvwBooks.AddRow("2", "The C# Helper Top 100", ...
    lvwBooks.AddRow("3", "Interview Puzzles Dissected", ...
    lvwBooks.AddRow("4", "C# 24-Hour Trainer, 2e", ...
    lvwBooks.AddRow("5", "Beginning Software Engineering", ...
    lvwBooks.AddRow("6", "Essential Algorithms", ...
    lvwBooks.AddRow("7", "Beginning Database Design Solutions", ...
}

The code selects the first ListView style (Large Icons) and sets the ListView control’s SmallImageList and LargeImageList properties. It then calls the MakeColumnHeaders extension method to create the ListView column headers. (See the code for details).

Next, the code clears the two ImageList controls and creates a series of images at two different sizes: 32×32 and 64×64 pixels. It creates a Pen and Brush and then loops through the values 1 to 7. For each value, it calls the DrawStar method to create large and small stars with different numbers of points and adds the images to the ImageList components. It uses the value of i as key strings for the ListView icons. (Download the example to see how the DrawStar method works.)

After is has created the images, the code calls the AddRow extension method to add rows of data to the ListView control. The first parameter is the key string that identifies the ListView icons for that row. The following code shows the AddRow method.

// Add a row to the ListView.
public static void AddRow(this ListView lvw, string key,
    string item_title, params string[] subitem_titles)
{
    // Make the item.
    ListViewItem new_item = lvw.Items.Add(item_title, key);

    // Make the sub-items.
    for (int i = subitem_titles.GetLowerBound(0);
             i <= subitem_titles.GetUpperBound(0);
             i++)
    {
        new_item.SubItems.Add(subitem_titles[i]);
    }
}

This method creates a new ListViewItem object. It passes the constructor the item’s text and its key string. The ListView control uses the key string to select the appropriate images to display from the ImageList controls that are referenced by its SmallImageList and LargeImageList properties.

The method then loops through its other parameters and creates the item’s sub-items. Download the example to see additional details.

My next post will show how to load images into an ImageList from a database.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in controls, drawing, graphics | Tagged , , , , , , , , , , , , , , , , | Leave a comment