Freitag, 21. Februar 2014

Kreisdiagramm zeichnen

Im heutigen Post möchte ich zeigen wie man in C# "per Hand", also durch Zeichnen über die Graphics Klasse, selber Kreisdiagramme (auch Kuchendiagramme genannt) zeichnet. Die Idee kam mir deswegen, weil ich in einer Android App ein solches zeichnen wollte, im Xamarin Studio für Android aber im Gegensatz zum Visual Studio kein Chart Steuerelement vorhanden ist.
Das Zeichnen erledigt die Klasse PieChart über die Funktion Draw(). Diese erwartet eine Liste von Tupeln als Parameter, von denen jeweils der erste Wert den Namen der entsprechenden Kategorie angibt, der zweite den Wert.
Die einzelnen Stücke des Kreises werden dann über die Funktion FillPie() gezeichnet, welcher man den Start- und Endwinkel des aktuellen Kreisbogens übergeben kann.
Die Funktion DetermineColor() bestimmt in Abhängigkeit des aktuellen Winkels die Farbe des Kreisbogens. Ich habe versucht, einen vollständigen Farbkreis darzustellen und hierzu für die Farben Rot, Grün und Blau jeweils eine quadratische Funktion approximiert, welche einen Wertebereich von 240° abdeckt. Damit sich benachbarte Kreisstücke farblich möglichst weit abheben, wird reihum zum Winkel für die Farbbestimmung 0°, 120° oder 240° dazuaddiert. Allerdings muss ich zugeben, dass ich mit dieser Farbverteilung nicht wirklich zufrieden bin, das lässt sich sicherlich besser lösen. Falls jemand eine interessante Funktion hierfür schreibt, würde ich mich sehr über einen Kommentar freuen.
Die ersten Werte der Tupel, die Namen der Kategorien, werden in der Klasse nicht benutzt, da ich mir dachte, dass eine eventuelle Beschriftung eh sehr individuell ausfallen wird. Für Anregungen kann man sich aber den Code für oben verlinkte App anschauen, dort drucke ich die Beschriftungen unter das Diagramm.
Nun der Code der Klasse:

    public class PieChart
    {
        int PosCounter = 0;

        private Color DetermineColor(float angle)
        {
            angle += PosCounter * 120;
            while (angle > 360)
                angle -= 360;

            PosCounter++;
            if (PosCounter > 2)
                PosCounter = 0;

            int Red;
            if (angle >= 270)
                Red = (int)((-0.000069 * Math.Pow((angle - 360), 2) + 1) * 255);
            else
                Red = (int)((-0.000069 * Math.Pow((angle), 2) + 1) * 255);
            Red = (Red > 0) ? Red : 0;
            Red = (Red > 255) ? 255 : Red;

            int Green = (int)((-0.000114 * Math.Pow((angle), 2) + 0.030682 * angle + -1.04545) * 255);
            Green = (Green > 0) ? Green : 0;
            Green = (Green > 255) ? 255 : Green;

            int Blue = (int)((-0.000069 * Math.Pow(((angle)), 2) + 0.033333 * angle - 3) * 255);
            Blue = (Blue > 0) ? Blue : 0;
            Blue = (Blue > 255) ? 255 : Blue;

            return Color.FromArgb(Red, Green, Blue);
        }

        public Bitmap Draw(List<Tuple<string,float>> data)
        {
            int PieSize = 400;
            float PrevStart = 0;

            Bitmap Result = new Bitmap(PieSize, PieSize);
            Graphics G = Graphics.FromImage(Result);

            float TotalValue = 0;
            for (int i = 0; i < data.Count; i++)
            {
                TotalValue += data[i].Item2;
            }

            for (int i = 0; i < data.Count; i++)
            {
                G.FillPie(new SolidBrush(DetermineColor(PrevStart)), new Rectangle(0, 0, PieSize, PieSize), PrevStart, (data[i].Item2 / TotalValue) * 360);
                PrevStart += (data[i].Item2 / TotalValue) * 360;
            }

            return Result;
        }
    }

Ein Beispielaufruf könnte so aussehen:
        private void pictureBox1_Click(object sender, EventArgs e)
        {
            PieChart A = new PieChart();
            List<Tuple<string, float>> B = new List<Tuple<string, float>>();

            B.Add(new Tuple<string, float>("A", 3));
            B.Add(new Tuple<string, float>("A", 2));
            B.Add(new Tuple<string, float>("A", 1));
            B.Add(new Tuple<string, float>("A", 2));
            B.Add(new Tuple<string, float>("A", 4));
            B.Add(new Tuple<string, float>("A", 3));

            pictureBox1.Image = A.Draw(B);
        }

Das Ergebnis sieht dann so aus:


Freitag, 14. Februar 2014

Standby Modus verhindern

Hört man zum Beispiel Musik oder schaut einen Film, bleibt der Windows Computer an und geht trotz fehlender Benutzereingabe nicht in den Energiesparmodus / Standby Modus über. Bei laufenden C# Anwendungen tut er dies allerdings.
Heute möchte ich zeigen, wie man dieses verhindert. Dafür müssen wir über P/Invoke die Funktion SetThreadExecutionState() einbinden, über welche man dem System mitteilen kann, dass das Programm es zur Zeit benötigt.
Gesetzt werden können unter anderem die folgenden 2 Status:

  • ES_DISPLAY_REQUIRED: Dieser Modus verhindert das Ausschalten des Displays.
  • ES_SYSTEM_REQUIRED: Dieser Modus verhindert das Abschalten des ganzen Systems.
Ruft man die Funktion nur mit einem dieser Status einzeln auf, wird lediglich der Timer für das jeweilige Event zurückgesetzt, das heißt ab dem Zeitpunkt des Aufrufs zählt das System wieder als unbenutzt und nach Ablauf der eingestellten Zeit schaltet sich der Bildschirm / das System aus.
Um das Abschalten komplett zu verhindern muss die Funktion entweder periodisch aufgerufen werden oder man verodert den Status beim Aufruf mit ES_CONTINOUS, so reicht ein einmaliger Aufruf.
Das folgende Beispielprogramm teilt beim Start dem Betriebssystem mit, dass es nie in den Standby Modus übergehen soll:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

using System.Runtime.InteropServices;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public enum EXECUTION_STATE : uint
        {
            ES_AWAYMODE_REQUIRED = 0x00000040,
            ES_CONTINUOUS = 0x80000000,
            ES_DISPLAY_REQUIRED = 0x00000002,
            ES_SYSTEM_REQUIRED = 0x00000001
        }

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags);

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS | EXECUTION_STATE.ES_SYSTEM_REQUIRED);
        }
    }
}

Montag, 3. Februar 2014

Texterkennung (OCR)

Auf Grund eines Leserkommentars habe ich mich etwas mit dem sehr interessanten Thema Texterkennung / Zeichenerkennung (im englischen "Optical Character Recognition" - OCR) beschäftigt und festgestellt, dass sich relativ einfach relativ gute Ergebnisse erreichen lassen.
Beim Thema OCR geht es um die automatische Erkennung von Texten und Zeichen aus Bildern - hier poste ich natürlich über OCR mittels C#.
Eine OCR Klasse selber zu schreiben, wäre nicht so einfach - allerdings gibt es viele fertige Klassen, auch für C#. Ich habe Puma.NET benutzt.
Zuerst einmal ist dieses von der obigen Homepage herunterzuladen. Möchte man Puma dann in einem Programm verwenden, muss man einen Verweis auf die Bibliothek "Puma.Net.dll" hinzufügen, welche sich bei mir im Ordner "C:\Program Files (x86)\Puma.NET\Assemblies" befindet.
Kompiliert man das eigene Projekt dann, müssen sich im Build Ordner die 3 Dateien "puma.interop.dll", "Puma.Net.dll" und "dibapi.dll" befinden. Letztere ist dort wahrscheinlich nicht vorhanden und muss deswegen aus dem Assembly Ordner von Puma manuell hineinkopiert werden.
Eine Sache ist noch einzurichten bevor man mit OCR arbeiten kann: Dem aktuellen Benutzer sind volle Rechte für den Ordner "COM Server" im Puma Verzeichnis zu geben.
Nun zum Code, schon mit 4 Zeilen kann eine komplette Texterkennung realisiert werden. Zur Einfachheit binden wir Puma.Net mittels using ein.
Dann müssen wir zuerst ein Objekt vom Typ PumaPage erstellen, diesem übergeben wir den Pfad zum einzulesenden Bild.
Als nächstes legen wir das Format der einzulesenden Zeichen fest, hier nehmen wir einfach TxtAscii. Über die Eigenschaft Language kann sogar die Sprache des einzulesenden Textes festgelegt werden.
Mit der Funktion RecognizeToString() wird dann versucht, Text aus dem Bild zu erkennen.
Hier der komplette Code:

PumaPage P1 = new PumaPage(@"C:\Users\Oliver\Documents\Visual Studio 2013\Projects\COCR\COCR\bin\Debug\Test.bmp");
P1.FileFormat = PumaFileFormat.TxtAscii;
P1.Language = PumaLanguage.German;
string Result = P1.RecognizeToString();

Bei maschinell geschriebenen Texten funktioniert das Programm sehr gut, wird die Schriftart aber etwas verschnörkelter lässt das Ergebnis sofort spürbar nach.