Wednesday, July 18, 2012

Multilingual Website - Template



In this article, I would like to introduce you a website template which can be used as a base structure of a multilingual website. You can download the template, and use for free for your projects.
I tried to keep the source code of the project as simple as possible for the readers. Nevertheless, I explain some of the methods and classes I used in the template to clarify their usage for those who wish to use it in their projects.
First of all, we need to define which languages are supported by our website. This can be done with the LanguageEnum:

public enum LanguageEnum
{
    /// <summary>
    /// Represents the Dutch language.
    /// </summary>
    NL = 1,
 
    /// <summary>
    /// Represents the Engliah language.
    /// </summary>
    EN,
 
    /// <summary>
    /// Represents the Russian language.
    /// </summary>
    RU,
 
    /// <summary>
    /// Represents the Ukrainian language. 
    /// </summary>
    UK,
 
    /// <summary>
    /// Represents the Persian language.
    /// </summary>
    PR
}

To simplify the pages code behind, I added a PageBase class which is derived from the System.Web.UI.Page and all other pages are derived directly from this class.
The heart of the code is based on the Language property defined in the top level master page. This property need to be set every time a page of the website is loading. So, we set it in the Page_Load event handler of the master page.

protected void Page_Load(object sender, EventArgs e)
{
    this.Language = this.GetLanguage(Request.QueryString["LangId"]);
 
    if (this.IsPostBack)
        return;
}

There is no need to say that every pages of the web site should be defined under this top level master page. You can change this master page considering the principles or define nested master pages if necessary.
There are some LinkButtons defined as the language selector. When a language selector is clicked, redirecting to the same page in the selected language would be the most favorite type of switching for the readers. For, in the Click event handler of each language selector, the SwithLanguage method is called to redirect the site to the same page in the target language:

private void SwitchLanguage(string language)
{
    Uri uri = new Uri(Request.Url.ToString());
    string path = uri.AbsoluteUri;
 
    path = Regex.Replace(path, "/(" + CachedData.Instance.RegexLanguageSelector + ")(/*)""/" + language + "/", RegexOptions.IgnoreCase);
    path = Regex.Replace(path, "(\\?|&)LangId=(" + CachedData.Instance.RegexLanguageSelector + ")"string.Empty, RegexOptions.IgnoreCase);
 
    if (!Regex.IsMatch(path, "/(" + CachedData.Instance.RegexLanguageSelector + ")(/*)", RegexOptions.IgnoreCase))
    {
        path = path.Insert(uri.GetLeftPart(UriPartial.Authority).Length, "/" + language + "/");
        path = Regex.Replace(path, "/(" + CachedData.Instance.RegexLanguageSelector + ")//""/$1/", RegexOptions.IgnoreCase);
    }
 
    Response.Redirect(path);
}

To fast loading of the pages, the content of the page elements have been kept in a singleton CachedData class. Each time the web application is started or HttpRunTime.Cache is cleared, the CachedData will be reloaded (I describe it later).
In the CachedData class, there is a list of PreDefinedStringItem which allows you to keep the content in memory. The list is filled by calling the LoadPreDefinedStrings:

private void LoadPreDefinedStrings()
{
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeNavigation", 
        "Home", 
        LanguageEnum.NL));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeNavigation", 
        "Home", 
        LanguageEnum.EN));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeNavigation", 
        "Главная", 
        LanguageEnum.RU));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeNavigation", 
        "Головна", 
        LanguageEnum.UK));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeNavigation", 
        "خانه", 
        LanguageEnum.PR));
 
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactNavigation", 
        "Contact", 
        LanguageEnum.NL));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactNavigation", 
        "Contact", 
        LanguageEnum.EN));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactNavigation", 
        "Контакты", 
        LanguageEnum.RU));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactNavigation", 
        "Контакти", 
        LanguageEnum.UK));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactNavigation", 
        "تماس", 
        LanguageEnum.PR));
 
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactContent", 
        "7 DAGEN PER WEEK 24 UUR BEREIKBAAR", 
        LanguageEnum.NL));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactContent", 
        "7 DAYS PER WEEK 24 HOUR PER DAY", 
        LanguageEnum.EN));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactContent", 
        "7 ДНЕЙ В НЕДЕЛЮ, 24 ЧАСА В ДЕНЬ", 
        LanguageEnum.RU));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactContent", 
        "7 ДНІВ НА ТИЖДЕНЬ, 24 ГОДИНИ НА ДЕНЬ", 
        LanguageEnum.UK));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "ContactContent", 
        "7 روز در هفته، 24 ساعت در روز", 
        LanguageEnum.PR));
 
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeContent", 
        "Plaats hier hoofdinhoud.", 
        LanguageEnum.NL));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeContent", 
        "Place main content here.", 
        LanguageEnum.EN));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeContent", 
        "Поместите основному содержанию здесь.", 
        LanguageEnum.RU));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeContent", 
        "Помістіть основному змісту тут.", 
        LanguageEnum.UK));
    this.preDefinedStrings.Add(new PreDefinedStringItem(
        "HomeContent", 
        "مطالب اصلی را اینجا قرار دهید.", 
        LanguageEnum.PR));
 
    this.resetPreDefinedStrings = false;
}

In the template, I filled the list of PreDefinedStringItem directly from the code, but you can read the content from your database and fill the list. It is up to you.
In the Global.asax.cs, we add a HttpRunTime.Cache task to force the application to reload the CachedData items every 600 seconds (10 minutes). Depend on the web server you use and its parameters, this time may be varied.

private void AddTask(string name, int seconds)
{
    onCacheRemove = new CacheItemRemovedCallback(this.CacheItemRemoved);
    HttpRuntime.Cache.Insert(
        name, 
        seconds, 
        null,
        DateTime.Now.AddSeconds(seconds), 
        Cache.NoSlidingExpiration,
        CacheItemPriority.NotRemovable, 
        onCacheRemove);
}
 
private void CacheItemRemovedTask(object arguments)
{
    CachedData.Instance.ReloadCachedItems();
}
 
private void CacheItemRemoved(string name, object sender, CacheItemRemovedReason reason)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(this.CacheItemRemovedTask));
 
    // do stuff here if it matches our taskname, like WebRequest
    // re-add our task so it recurs
    this.AddTask(name, Convert.ToInt32(sender));
}

The CacheItemRemovedTask calls the CachedData.ReloadCachedItems which forces the CachedData class to reload the pre-defined strings the first time the content property is accessed then after.
The application is checking for the Request.Path and changes and rewrite it to a format, so the user can open a page by adding the language code after the root URL path. For example, http://localhost/en/default.aspx will open the home page in English language. This process is done in the Application_BeginRequest event handler:

protected void Application_BeginRequest(object sender, EventArgs e)
{
    string path = Request.Path;
    string defaultLanguage = "NL";
 
    this.SetRootURL();
 
    if (!Regex.IsMatch(
        path, "^/(" + CachedData.Instance.RegexLanguageSelector + ")", 
        RegexOptions.IgnoreCase) &&
        Regex.IsMatch(
        path, @"^(/(.+).aspx)", 
        RegexOptions.IgnoreCase))
    {
        path = "/" + defaultLanguage + path; 
        Response.Redirect(path);
    }
    else
    {
        if (Regex.IsMatch(
            path, "^/(" + CachedData.Instance.RegexLanguageSelector + ")/(.+).aspx", 
            RegexOptions.IgnoreCase))
        {
            path = Regex.Replace(path, 
                "^/(" + CachedData.Instance.RegexLanguageSelector + ")/(.+).aspx""/$2.aspx?LangId=$1", 
                RegexOptions.IgnoreCase);
            Context.RewritePath(path);
        }
        else if (Regex.IsMatch(
            path, "^/(" + CachedData.Instance.RegexLanguageSelector + ")(/*)\\z", 
            RegexOptions.IgnoreCase))
        {
            path = Regex.Replace(path, "^/(" + 
                CachedData.Instance.RegexLanguageSelector + ")(/*)\\z""/default.aspx?LangId=$1", 
                RegexOptions.IgnoreCase);
            Context.RewritePath(path);
        }
    }
}

I put the styles of the pages elements in Common.css file. There may be some additional styles which are exclusively available for a specific language. These styles can be added to the files which their names are <LanguageCode>.css. For example, if you have a right to left language available in your site, simple add PR.css to your site. This file will be added to the master page aspx content by the LanguageClass property:

public string LanguageClass
{
    get { return "<link href=\"/css/" + Enum.GetName(this.Language.GetType(), this.Language) + ".css\" rel=\"stylesheet\" />"; }
}

for every element of the pages content, you need to add a property to the master page code behind and use the CachedData.Instance.GetPreDefinedStringValue method to get the content of the element from the CachedData. The value should be loaded to the list of PredefinedStrings of the CachedData using the LoadPreDefinedStrings method which I explained it before.
As an example, if you want to have the text of the home navigation button in different languages, Add the HomeNavigation property to the master page code behind:

public string HomeNavigation
{
    get 
    { 
        return CachedData.Instance.GetPreDefinedStringValue("HomeNavigation"this.Language); 
    }
}

and to load it in the master page, simply call it by <%=HomeNavigation%> in the master page aspx file. This way, you can load the contents which are placed in the master page in different languages.

<a href="Default.aspx"><%=HomeNavigation%></a>

The same way, you can load the contents which are defined in other pages of the site. For example, to load the main content of the home page, add HomeContent property in the PageBase (or other pages) code behind:

public string HomeContent
{
    get 
    { 
        return CachedData.Instance.GetPreDefinedStringValue(
            "HomeContent", 
            ((SiteMasterPage)this.Master).Language); 
    }
}

and call it from the default page aspx file by <%=HomeContent%>.

<span class="mainContent"><%=HomeContent%></span>

When the application is loaded, it may be dropped by the web server after a period of idle time. To make the application available all the time, in the Application_End event handler, I force the application to open one of its pages, so the web server is forced to reload the application. Of course, depend on the web server and other parameters, this may not be worked all the time:

protected void Application_End(object sender, EventArgs e)
{
    this.LoadRootUrl();
}

I hope this would be helpful. Any comments would be highly appreciates.
Enjoy programming ;)




Saturday, June 2, 2012

File Extension Association in Setup Factory (No restart needed)

When installing our software, usually, we need to associate a file extension to the files which the software exclusively worked with them. It is simple and straightforward in Setup Factory. You just need to add the proper values in the windows registry. To do this, add the below code to the "On Post Install" actions of the Setup Factory. (I use version 9)


Registry.SetValue(HKEY_CLASSES_ROOT, ".ext", "", "Software Name", REG_SZ);
Registry.SetValue(HKEY_CLASSES_ROOT, "Software Name\\shell\\open\\command", "", SessionVar.Expand("\"%AppFolder%\\MyApplication.exe\" \"%1\""), REG_SZ);
Registry.SetValue(HKEY_CLASSES_ROOT, "Software Name\\DefaultIcon", "", SessionVar.Expand("%AppFolder%\\MyApplication.exe,0"), REG_SZ);


Where ".ext" is the file extension for your software data files, "Software Name" is the title of your software, and "MyApplicaton.exe" is the executable file which loads the data files as its parameter. I supposed that you already added the icon of your software to the Setup Factory as DefaultIcon.
Everything is done with the above piece of code; but you need to restart your computer for the file association to work with the new extension. 


There is another solution to complete the task directly from the Setup Factory without restarting the windows. You just need to add the below line to the above code:


DLL.CallFunction(_SystemFolder.."\\Shell32.dll", "SHChangeNotify", "134217728, 0, 0, 0", DLL_RETURN_TYPE_LONG, DLL_CALL_STDCALL); 


That's all. Everything should be worked fine now.









Thursday, May 17, 2012

Comodo Prevents Applications to Modify Windows Registry

Modifying Windows registry is a common task for applications. The installed Comodo Internet Security as the anti virus on my computer.  Each time I compile a native application and running it via the Embarcadero C++Builder, Comodo shows me a "Unrecognized Application" message and runs it in the Sand Box. 


I always click on the "Don't isolate it again" link on the Comodo message box. Usually after a few times of clicking on the mentioned link, Comodo will not show the message again. I thought that it remembers my answer and lets the application to run normally. 


When I called the functions which try to modify Windows registry, I noticed that it can not open the registry at all. This problem wastes lots of my time. I found that Comodo runs the application in the Sand Box, so it can not read or modify Windows registry keys values.


If you have such a kind of problem, check you anti virus which may prevent the application to have access to the Windows registry. For me, I disable the Defence+ feature of Comodo to be able to run the application normally when programming.



Sunday, May 6, 2012

Manage and Remove TRibbon pages and groups


When working with Embarcadero TRibbon component, usually we need to make several changes in the ribbon items including removing a ribbon page, making invisible the ribbon groups and items, and repositioning still visible ribbon groups.

Unfortunately, I couldn’t find straightforward methods in the Embarcadero’s TRibbon component to make these tasks easy to do (I am using C++Builder XE2 Update 3). So, I wrote some methods to solve them:

In this article, I supposed that you have an instance of TRibbon (named Ribbon1) on the current form (Form1) and all of the below methods are members of the form.

To remove a ribbon page (tab) from the ribbon, you can simply use the RemoveTab method of TRibbon class; but if you try to remove a page which has one or more ribbon groups, you will get unexpected access violation error messages. So you need to remove all of the groups in the page before removing the page:

void __fastcall TForm1::RemoveRibbonPage(TRibbonPage *Page)
{
    for(int i = Page->GroupCount - 1; i >= 0; i--)
        RemoveGroupItems(Page, (TRibbonGroup *)Page->Groups[i], false, true);

    Ribbon1->RemoveTab(Page->Caption);
}
//---------------------------------------------------------------------------

To remove ribbon group items, use the following method:

void __fastcall TForm1::RemoveGroupItems(TRibbonPage *Page, TRibbonGroup *Group, bool KeepVisible, bool RemoveIfEmpty)
{
    for(int j = Group->Items->Count - 1; j >= 0; j--)
    {
        if(!KeepVisible || !Group->Items->ActionClients[j]->Visible ||
           (Group->Items->ActionClients[j]->Separator && (j == Group->Items->Count - 1 || !j)))
            Group->Items->Delete(j);
    }

    RemoveExtraSeparators(Page, Group);

    if(RemoveIfEmpty && Group->Items->Count == 0)
        Page->RemoveControl(Group);
}
//---------------------------------------------------------------------------

The KeepVisible parameter indicates whether all of the group items should be removed, or just non-visible ones.
The RemoveIfEmpty parameter indicates whether to remove the ribbon group if there is no item available

When you delete only non-visible group items, some separator items may be still available as the first or last items of the group. They will be removed by calling the below method:

void __fastcall TForm1::RemoveExtraSeparators(TRibbonPage *Page, TRibbonGroup *Group)
{
    for(int j = Group->Items->Count - 1; j >= 0; j--)
    {
        if(Group->Items->ActionClients[j]->Separator &&
           (j == Group->Items->Count - 1 || !j))
            Group->Items->Delete(j);
    }
}
//---------------------------------------------------------------------------

When removing ribbon groups from a page, you may noticed that the width of the ribbon groups which have non-visible items will not be decreased, and you will see some empty spaces in the group. You need to permanently delete the group item from the memory. The below method, scans all of the pages, and removes all of the non-visible group items. By calling this method, extra group spaces will be collapsed.

void __fastcall TForm1::RemoveNonVisibleRibbonItems(void)
{
    TRibbonPage *Page;

    for(int i = 0; i < Ribbon1->Tabs->Count; ++i)
    {
        Page = (TRibbonPage *)Ribbon1->Tabs->Items[i]->Page;
        for(int j = Page->GroupCount - 1; j >= 0; j--)
        RemoveGroupItems(Page, (TRibbonGroup *)Page->Groups[j], true, true);
    }
}
//---------------------------------------------------------------------------

When you remove a group from a page, the group will be removed from the memory (I guess), but the basic information of the group will be kept in the page’s Groups list, so you will see empty spaces at the position of the removed ribbon groups. To correct this problem, simply change the index of the removed (non-visible) groups to put them at the end of the Groups items. This way, all of the groups will be arranged completely:

void __fastcall TForm1::MoveNonVisibleRibbonGroups(void)
{
    TRibbonPage *Page;
    int Index;
    int Counter;

    for(int i = 0; i < Ribbon1->Tabs->Count; ++i)
    {
        Page = (TRibbonPage *)Ribbon1->Tabs->Items[i]->Page;
        Index = FindMaxRibbonGroupIndex(Page) + 1;

        for(int j = 0; j < Page->GroupCount; ++j)
        {
            Counter = j;
            while(!Page->Groups[j]->Visible && Counter++ < Page->GroupCount)
            Page->Groups[j]->GroupIndex = ++Index;
        }
    }
}
//---------------------------------------------------------------------------

To set a group as the last item, of the ribbon page’s Groups list, you need to find the last index number used by the groups. Use FindMaxRibbonGroupIndex method to get the last index:

int __fastcall TForm1::FindMaxRibbonGroupIndex(TRibbonPage *Page)
{
    int ReturnValue = -1;

    for(int j = Page->GroupCount - 1; j >= 0; j--)
        ReturnValue = MAX(ReturnValue, Page->Groups[j]->GroupIndex);

    return ReturnValue;
}
//---------------------------------------------------------------------------

As a recommendation, you set the visibility of the group items you want to keep them on the ribbon, and call the below commands to arrange all of the remaining groups and group items completely:

RemoveNonVisibleRibbonItems();
MoveNonVisibleRibbonGroups();

Any comments on this article would be highly welcomed.


Monday, April 30, 2012

TRibbon.MoveGroup Method Parameters

Yesterday, I was working on a program which have several editions. I created the application ribbon bar at design time, but in one of the editions I had to combine two of the ribbon pages. so I tried to move all of the ribbon groups from one ribbon page to another using the TRibbon.MoveGroup method.

I was completely, unsuccessful as I always passed the name of the source group and the target page as the parameters. The method definition is this: 

void __fastcall MoveGroup(const System::UnicodeString GroupName, const System::UnicodeString PageName);

I realized that the parameters should be the Caption (not the name) of the moving ribbon group and the target ribbon page. This way the method works charmingly :) 



Thursday, April 26, 2012

Add Node/Child Node Icons

Today, I was googling a lot to find good icons for "Add Node" and "Add Child Node" commands of my application. I found some, but good ones were 16x16 in size. So, I tried to draw them myself:

I hope they can be useful for anybody who need them. Use them freely.



Wednesday, April 25, 2012

Convert to Base64 using TIdEncodeMIME when upgrading the compiler


As useful task for data streaming, we need to encode some parts of our database records to Base64 encoding format. For example, to save pictures in text base databases (such as XML), we do need to convert it to Base64 text format.
In Borland Developer Studio 2006, you can use Encode(String) method of the TIdEncodeMIME class to convert binary information into Base64 format. When upgrading to a newer version of C++Builder (or Delphi), for example Embarcadero RAD Studio XE2, you must use a newer method, EncodeStream(TStream *) instead of the Encode(String). In the new TIdEncoderMIME class, convering binary data using Encode method may cause unexpected error.

//In this example the Data->Picture is of type TMemoryStream
TIdEncoderMIME *Encoder = new TIdEncoderMIME(NULL);
ReturnValue += Encoder->EncodeStream(Data->Picture); //instead of Encode method
delete Encoder;



Monday, March 26, 2012

GraphicsPath.IsVisible() method wont work when using zoom matrix with a big zoom factor in GDI+

When programming for a graphical application, I noticed that the GraphicsPath.IsVisible() method, sometimes,  is not working fine for small path figures when using scale matrix for the Graphics of the object or Control.

I googled for the problem and found a solution in Java. Then I translated the code to C# as an extension method to the GraphicsPath. It works fine now. The translated code is as follows:

/// <summary>
/// Indicates whether the graphics path contains the specified point or not.
/// </summary>
/// <param name="graphicsPath">The graphics path to check if it contains the point.</param>
/// <param name="location">The specified location to check if it is inside the path or not.</param>
/// <returns>True if the point is within the graphics path, otherwise false.</returns>
public static bool Contains(this GraphicsPath graphicsPath, PointF location)
{
    GraphicsPath actualPath = (GraphicsPath)graphicsPath.Clone();
    bool oddTransitions = false;

    actualPath.Flatten(null, .25f);

    for (int i = 0, j = actualPath.PointCount - 1; i < actualPath.PointCount; j = i++)
        if ((actualPath.PathPoints[i].Y < location.Y && actualPath.PathPoints[j].Y >= location.Y) ||
            (actualPath.PathPoints[j].Y < location.Y && actualPath.PathPoints[i].Y >= location.Y))
            if (actualPath.PathPoints[i].X + 
                ((location.Y - actualPath.PathPoints[i].Y) /
                (actualPath.PathPoints[j].Y - actualPath.PathPoints[i].Y) *
                (actualPath.PathPoints[j].X - actualPath.PathPoints[i].X)) < location.X)
                oddTransitions = !oddTransitions;

    return oddTransitions;
}

I cloned and flattened the original path to be sure the result is correct even if it contains Beziers. Hope it can help you.

Enjoy programming.


C# Methods for +18 !!!

Today, while programming, I was very busy with a problem. When I trying to use StringBuilder.Append method, IntelliSence showed me necessary information about the method: 










Suddenly, I focused to the (+ 18 overloads)
Wow, can I use this method? Am I over 18? Yes, I can! 
These are questions and answer I asked myself when I saw that. 


No worry, everybody can use the method at anytime ;-) 


Enjoy programming!



Sunday, March 11, 2012

Minimum scale factor for the shapes in vector graphical applications

There are two scenarios when scaling shapes in graphical applications:
  1. The application always keeps the original values of the shape including all of the changes applied on it. In this situation, there is no matter how much the minimum scale factor should be. At any time, the original values can be used to change the shape.
  2. The application uses the latest shape values for each change. Obviously, when you scale the shape and set the scale factor to 0 (horizontally and/or vertically), the shape width and/or height will be changed to 0 too. So, if you try to apply another changes on the shape (e.g. try to rescale it to a bigger shape), the width and/or height of the will not be changed as they are now 0 and multiplication of them to any scale factor will be 0 too.
    In this scenario, you should consider a minimum scale factor to prevent disappearing the shape. 

Saturday, March 10, 2012

How to zoom in GDI+ painting?

When you are creating a GDI+ painting control, you may need to add some facilities to zoom in the painting area and scroll inside it.
It's simple. I show you how to do it with a few lines of code:
I supposed that the amount of zooming is set to the zoomFactor variable. First of all, you need to transform the Graphics object of the OnPaint event handler, and the amount of the scrolls are set as the value of your scrollable control horizonal and vertical scroll bar values.
So add the below code to the overriden OnPaint event handler:

/// <summary>
/// Occures when the entire control needs repainting.
/// </summary>
/// <param name="e">A PaintEventArgs that contains the event data.</param>
protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);
    e.Graphics.Transform = this.SetGraphicsMatrix();
    e.Graphics.TranslateTransform(
        (float)Math.Round(
            this.Unzoom(AutoScrollPosition.X) - AutoScrollPosition.X, 
            MidpointRounding.AwayFromZero),
        (float)Math.Round(
            this.Unzoom(AutoScrollPosition.Y) - AutoScrollPosition.Y, 
            MidpointRounding.AwayFromZero));
    // Your paintings here
}
 
SetGraphicsMatrix() creates a matrix to scale the painting area, and synchronizes thetop-left corner of the painting area with the horizontal and vertical scroll bars:
 
/// <summary>
/// Creates a matrix to scale the painting area and synchronize
/// the top-left corner of the control with the scroll bars.
/// </summary>
/// <returns>The translated matrix.</returns>
private Matrix SetGraphicsMatrix()
{
    Matrix matrix = new System.Drawing.Drawing2D.Matrix();
    matrix.Scale(this.zoomFactor, this.zoomFactor);
            matrix.Translate(-this.HorizontalScroll.Value, 
                             -this.VerticalScroll.Value);
    return matrix;
}
 
Using the above codes, your painting will be drawn and zoomed correctly on your scrollable control. 





What if you need to obtain the location of a point on your scrolled and zoomed control?
The Unzoom method calculates a value describing its original value when the painting area is unzoomed (zoomFactor = 1). Below you can find two overloads of it:
 
/// <summary>
/// Calculates the original value of a float value when zooming.
/// </summary>
/// <param name="value">The value which its original value 
/// should be calculated.</param>
/// <returns>The original value of the float value when zooming.</returns>
internal float Unzoom(float value)
{
    return value / this.zoomFactor;
}


/// <summary>
/// Calculates the original values of a location point when zooming.
/// </summary>
/// <param name="location">The location which its original values 
/// should be calculated.</param>
/// <returns>The original values of the location when zooming.</returns>
internal PointF Unzoom(PointF location)
{
    return new PointF(location.X / this.zoomFactor, 
                      location.Y / this.zoomFactor);
}
 
And finaly, you may need to know the location of a point on your control considering the scroll values. To do this, Use ScrolledLocation method:
 
/// <summary>
/// Calculates the correct values of a location
/// when Canvas2D control is scrolled.
/// </summary>
/// <param name="location">The specified location.</param>
/// <returns>The calculated location after scrolling.</returns>
internal PointF ScrolledLocation(PointF location)
{
    return new PointF(location.X - AutoScrollPosition.X, 
        location.Y - AutoScrollPosition.Y);
}
 
Although ScrolledLocation method calculates the location of a point when your control is scrolled, it does not work correctly when the zoomFactor is not equal to 1. To resolve this problem, you need to combine it with Unzoom method.
For example: To get the correct location of mouse when you click on the control, use the below piece of code:

PointF PaintLocation = this.Unzoom(
     this.ScrolledLocation(this.PointToClient(MousePosition)));
 
That's all. Enjoy your programming.