Tech Blech

Thursday, May 20, 2010

The challenge of printing a .NET textbox

On Yahoo Answers, someone asked how to print the content of a RichTextBox control in .NET. I did finally manage to print a (simpler) TextBox control in .NET, and that was difficult enough. I am documenting that here for myself or anyone else
facing this challenge.

First, a little rant. Before the .NET framework was released (~2000), Microsoft provided a handy .print method on the RichTextBox and TextBox controls, and all a programmer needed to do was call it. But in Microsoft's almighty wisdom (and trying to be just like Java), the simple and highly useful .print method was removed in .NET, and now you have to do all the following steps successfully. And note, it's just as difficult in Java--what were the language developers thinking? I imagine they were thinking to provide maximum flexibility, but why not provide a quick-and-dirty out for the rest of us?

In the example below, my form (called JobForm) is printing the contents of a TextBox control (called textBoxRight).

STEP 1: DECLARE VARIABLES

You need a bunch of special fields in your form code to keep track of printing information. To get started, just use this:

#region printing declarations

        private Font midlistRegular = new Font(
            "San Serif",
           (float)7.8,
           FontStyle.Regular,
           GraphicsUnit.Point);

        private Font midlistRegular1 = new Font(
            "San Serif",
           (float)7.6,
           FontStyle.Bold,
           GraphicsUnit.Point);

        private int printMargin = 1;

        private int lastPosition = 0;

        private int lastIndex = 0;

        /// 
        /// Set in Paint (of form) for use when printing
        /// 
        private float screenResolutionX = 0;

        #endregion printing declarations

STEP 2: INSTANTIATE A PRINTDOCUMENT OBJECT AND CREATE ITS EVENT CODE

You need a special object that raises an event each time one page has been printed and causes the next page to be printed. It is a non-visible control and is called "PrintDocument" (in library System.Drawing.Printing).

In the Windows Designer, drag a non-visible "PrintDocument" control onto your form (System.Drawing.Printing.PrintDocument). It will be instantiated on your form as "printDocument1". Double-click the PrintDocument control on the form to create the "PrintPage" event and give it the following code (using your TextBox name instead off "textBoxRight"):

private void printDocument1_PrintPage(object sender, PrintPageEventArgs e)
        {
            try
            {
                e.HasMorePages = this.PrintOnePage(
                    e.Graphics,
                    this.textBoxRight,
                    this.printDocument1,
                    this.screenResolutionX);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

STEP 3:

Now create the PrintOnePage() method needed by the above code. Although the display truncates this code, if you copy it using Control-C, all the code will be grabbed. Use the boilerplate code below unchanged (and I apologize that it's so ugly):

/// 
        /// Goes through each line of the text box and prints it
        /// 
        private bool PrintOnePage(Graphics g, 
                 TextBox txtSurface, PrintDocument printer, 
                 float screenResolution)
        {
            // Font textFont = txtSurface.Font;
            Font textFont = 
                   new Font("San serif", 
                            (float)10.0, FontStyle.Regular, 
                            GraphicsUnit.Point);

            // go line by line and draw each string
            int startIndex = this.lastIndex;
            int index = txtSurface.Text.IndexOf("\n", startIndex);

            int nextPosition = (int)this.lastPosition;
            // just use the default string format
            StringFormat sf = new StringFormat();

            // sf.FormatFlags = StringFormatFlags.NoClip | (~StringFormatFlags.NoWrap );
            // get the page height
            int lastPagePosition = (int)(((printer.DefaultPageSettings.PaperSize.Height / 100.0f) - 1.0f) * (float)screenResolution);
            // int resolution = printer.DefaultPageSettings.PrinterResolution.X;

            // use the screen resolution for measuring the page
            int resolution = (int)screenResolution;

            // calculate the maximum width in inches from the default paper size and the margin
            int maxwidth =
                (int)((printer.DefaultPageSettings.PaperSize.Width / 100.0f - this.printMargin * 2) * resolution);

            // get the margin in inches
            int printMarginInPixels = resolution * this.printMargin + 6;
            Rectangle rtLayout = new Rectangle(0, 0, 0, 0);
            int lineheight = 0;

            while (index != -1)
            {
                string nextLine = txtSurface.Text.Substring(startIndex, index - startIndex);
                lineheight = (int)(g.MeasureString(nextLine, textFont, maxwidth, sf).Height);
                rtLayout = new Rectangle(printMarginInPixels, nextPosition, maxwidth, lineheight);
                g.DrawString(nextLine, textFont, Brushes.Black, rtLayout, sf);

                nextPosition += (int)(lineheight + 3);
                startIndex = index + 1;
                index = txtSurface.Text.IndexOf("\n", startIndex);
                if (nextPosition > lastPagePosition)
                {
                    this.lastPosition = (int)screenResolution;
                    this.lastIndex = index;
                    return true; // reached end of page
                }
            }

            // draw the last line
            string lastLine = txtSurface.Text.Substring(startIndex);
            lineheight = (int)(g.MeasureString(lastLine, textFont, maxwidth, sf).Height);
            rtLayout = new Rectangle(printMarginInPixels, nextPosition, maxwidth, lineheight);
            g.DrawString(lastLine, textFont, Brushes.Black, rtLayout, sf);

            this.lastPosition = (int)screenResolution;
            this.lastIndex = 0;
            return false;
        }

STEP 4: ADD CODE TO YOUR FORM'S PAINT EVENT

In Windows Designer, open your form in graphical view mode. Open the form's Properties Window and click the lightning bolt to see events. Double-click on the form's Paint event to create it, and paste the boiler-plate code from my Paint event below into your form's paint event (your event will have a different name, using your form's name, than mine does below):

private void JobForm_Paint(object sender,
                            System.Windows.Forms.PaintEventArgs e)
        {
            // save the form height here
            this.screenResolutionX = e.Graphics.DpiX;

            // set the last position of the text box
            this.lastPosition = (int)this.screenResolutionX;
        }

STEP 5: ACTUALLY PRINT THE TEXTBOX

To actually print the contents of the TextBox, you'll need code like this in a print menu or button event:

PrintDialog printDialog1 = null;
 printDialog1 = new PrintDialog();
        if (printDialog1.ShowDialog() == DialogResult.OK)
        {
             this.printDocument1.PrinterSettings = printDialog1.PrinterSettings;
             this.printDocument1.Print();
        }

I haven't paid huge attention to all the above code, once I got it working. I snarfed much of it from various sources on the web (thank you, bloggers!). It could be enhanced or cleaned up a lot. Maybe this will help another programmer get it done.

Labels: ,

Thursday, March 04, 2010

Wrestling to sort the ASP.NET GridView

It's supposed to be easy, and practically codeless to use, and it is--sometimes. When a GridView is to be populated with the same dataset every time the page loads. But I had a drop-down list where a condition had to be selected, and based on that, the grid then had to be populated. I made it to the point where I got the dropdown selecting, and the grid populating, but there were 2 problems: paging didn't work, and sorting didn't work. I decided to turn paging off, so sorting was my last remaining issue.

Umpteen useless web articles later, I resorted to the paper books stashed on my shelf at home. First stop was Murach's ASP.NET 2.0, which is alleged to be so good. But it held no love for me. Second stop was Dino Esposito's "Programming Microsoft ASP.NET 2.0: Core Reference"--and finally, I got the help I needed.

I'm blogging about this because a good book deserves real credit. Many mysteries were unraveled by the Esposito book, including that I needed to explicitly re-populate the GridView when its "Sorting" event fired. Esposito's directions were extremely explicit: use the "Sorting" event's GridViewArgEvents ("e" parameter) to find out the sort key, and write a special stored procedure that uses the sort key to ORDER the data differently. These last bits of information were the treasure that finally allowed me to get sorting to work.

I'm posting a copy of the rather odd-looking stored procedure that I ended up using below for your edification. The "@order_by" parameter names the column on which to sort, and the odd way of constructing the query from strings allows brackets to fit around any strange or keyword column names:

CREATE PROCEDURE [dbo].[lsp_NRSA_find_counts_and_person_by_naded] 
@naded_id int,
@order_by nvarchar(12)
AS

BEGIN

 IF @order_by = ''
 BEGIN
  SET @order_by = 'slide'
 END
 
 EXEC ('SELECT * ' + 
 'FROM vw_NRSA_cemaats_by_count_and_person ' +
 'WHERE naded = ' + @naded_id + 
 ' ORDER BY [' + @order_by + ']')

END

Labels: , ,

Sunday, May 03, 2009

C# code to get schema of an Access table

I just wanted to dump the schema of a table in Microsoft Access. There is a lot of code on the web which purports to do this, but most of it didn't actually work, and most of it was not in C# (my current preferred language). So I am posting this here for anyone that needs it. Just change the path to the database file in the first line of code, and the name of the table ("taxa" in my example) in the second line of code. This program dumps the table schema to a file created by the LogFile object (also attached below) located in the application folder. You'll need to add "using System.Data.OleDb;" to the top of the file. Or, just download the code here. The code:




OleDbConnection conn =
 new OleDbConnection(
    "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" +
    "C:\\phycoaide\\phycoaide.mdb;Persist Security Info=False;");

// retrieving schema for a single table
OleDbCommand cmd = new OleDbCommand("taxa", conn);
cmd.CommandType = CommandType.TableDirect;
conn.Open();
OleDbDataReader reader =
 cmd.ExecuteReader(CommandBehavior.SchemaOnly);
DataTable schemaTable = reader.GetSchemaTable();
reader.Close();
conn.Close();

LogFile.WriteLine(" ");
foreach (DataRow r in schemaTable.Rows)
{
 LogFile.WriteLine(" ");
 foreach (DataColumn c in schemaTable.Columns)
 {
    LogFile.WriteLine(c.ColumnName + ": " + r[c.ColumnName]);
 }
}
MessageBox.Show("done");


The LogFile class creates a file in the application folder from which your program runs:




// 
// No copyright; free for reuse by anyone
// 
// Pat G. Palmer
// ppalmer AT harbormist.com
// 2009-05-04
// Opens a single-threaded log file to trace execution
namespace Ansp
{
    using System;
    using System.IO;            // file readers and writers
    using System.Windows.Forms; // Application object

    /// 
    /// Singleton that appends log entries to a text file
    /// in the application folder.  If the file grows
    /// to be too large, it deletes itself and starts over.
    /// The file is kept open until the application ends
    /// and implements the "dispose" pattern in case things
    /// do not end gracefully.
    /// 
    public class LogFile : IDisposable
    {
        private static int maxsize = 470000;
        private static string fileSuffix = "_log.txt";
        private static string fileSpecification;
        private static StreamWriter filewriter;
        private static LogFile instance;

        private LogFile()
        {
        }

        ~LogFile()
        {
            this.Dispose(false);
        }

        public static void InitLogFile()
        {
            if (instance == null)
            {
                instance = new LogFile();
            }

            string stringMe = "InitLogFile: ";
            try
            {
                if (Application.ProductName.Length == 0)
                {
                    fileSpecification = Application.StartupPath + "\\" +
                       "Test" + fileSuffix;
                }
                else
                {
                    fileSpecification = Application.StartupPath + "\\" +
                       Application.ProductName + fileSuffix;
                }

                // restart file if too big
                if (File.Exists(fileSpecification))
                {
                    FileInfo myFileInfo = new FileInfo(fileSpecification);
                    if (myFileInfo.Length > maxsize)
                    {
                        File.Delete(fileSpecification);
                    }

                    myFileInfo = null;
                }

                // restart file with appending
                filewriter = new StreamWriter(
                   fileSpecification, true, System.Text.Encoding.UTF8);

                // start log with standard info
                WriteLine("\r\n---------------------------------------------");
                string tempString = stringMe +
                    Application.ProductName + " " +
                    Application.ProductVersion +
                    "log opened at " + 
                    DateTime.Now;
                WriteLine(tempString);
                WriteLine(stringMe + "username=" + SystemInformation.UserName);
                WriteLine(stringMe + Application.StartupPath);
            }
            catch
            {
            }
        }

        public static void WriteLine(string myInputLine)
        {
            try
            {
                if (instance == null)
                {
                   InitLogFile(); // first time only
                }
                if (myInputLine.Length != 0)
                {
                   filewriter.WriteLine(myInputLine);
                   filewriter.Flush(); // update file
                }
            }
            catch
            {
            }
        }

        public static void Close()
        {
            instance.Dispose();
        }

        /// 
        /// Implement IDisposable.
        /// Do not make this method virtual.
        /// A derived class must not override this method.
        /// 
        public void Dispose()
        {
            this.Dispose(true);
            //// Now, we call GC.SupressFinalize to take this object
            //// off the finalization queue and prevent finalization
            //// code for this object from executing a second time.
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                // no managed resources to clean up
            }
            if (instance != null)
            {
                if (filewriter != null)
                {
                    try
                    {
                        filewriter.Flush();
                        filewriter.Close();
                    }
                    catch
                    {
                    }

                    filewriter = null;
                } // end if filewriter not null
            } // end if instance not null
        }

    } // end class LogFile()
} // end namespace

Labels: ,