Autor Beitrag
jfheins
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 918
Erhaltene Danke: 158

Win 10
VS 2013, VS2015
BeitragVerfasst: Fr 04.01.13 22:03 
Hallo und frohes Neues :)
Ich möchte ein GUI Programm schreiben, dass mit einem Mikrocontroller kommuniziert. Soweit kein Problem: es gibt ein Protokoll und die Daten werden einfach binär an einen seriellen Port 'rausgeschrieben. Das Problem ist der GUI Aufbau.
Erstmal die Datenstruktur, die zu befüllen ist:
ausblenden volle Höhe C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
    public enum COMM_Type : byte
    {
        // Allgemeine Datentypen
        PING = 0x00,
        ACK = 0x01,

  // Requests
  COMM_REQ_STATUS          = 0x10,
  COMM_REQ_MOTORSPEED        = 0x11,
  COMM_REQ_SENSORDATA        = 0x12,
  COMM_REQ_DRIVEN_DISTANCE    = 0x13,
  COMM_REQ_ROPE_SENSOR_STATE    = 0x15,
  COMM_REQ_TEMPERATURE      = 0x16,

  // Responses
  COMM_RES_STATUS          = 0x50,
  COMM_RES_MOTORSPEED        = 0x51,
  COMM_RES_SENSORDATA        = 0x52,
  COMM_RES_DRIVEN_DISTANCE    = 0x53,
  COMM_RES_ROPE_SENSOR_STATE    = 0x55,
  COMM_RES_TEMPERATURE      = 0x56,

  // Kommandos
  COMM_CMD_START          = 0x100,
  COMM_CMD_STOP          = 0x101,
  COMM_CMD_SETSPEED        = 0x102,
  COMM_CMD_SETSPEED_RPM      = 0x103,
  COMM_CMD_SETSPEED_PROMILLE    = 0x104,
  COMM_CMD_NOTAUS          = 0x105,
  
  //RAW Payload zB für Debug strings
  COMM_RAW_PAYLOAD        = 0x200
    }

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct COMM_Packet
    {
        public UInt16 ASM;      //< Attached Synchronization Marker
        public Byte Destination;  //< Ziel-Adresse
        public Byte Source;      //< Quell-Adresse
        public UInt32 Timestamp;    //< Zeitstempel des Pakets
        public COMM_Type Type;      //< Typ des Pakets
        public Int32 Data;      //< Paketdaten
        public Byte PayloadLength;  //< Länge der Payload-Daten
    }


Es gibt also Anforderungen (Request) auf die man eine Antwort erwartet, Antworten und Befehle (auf die nur mit einem ACK geantwortet wird.)
Man beachte z.B. dass SetSpeed mehrfach vorkommt: In der einfachen Variante wird die Geschw. in "m/s" gesetzt und geregelt. RPM regelt auf U/min statt auf m/s und Promille deaktiviert die Regelung und kann von -1000 bis 1000 gehen.

Jetzt soll das GUI Menü ungefähr so aussehen:
1. Combobox mit Auswahl "Was will ich?" also z.B. "Ping, Sensor auslesen, Variable auslesen, Variable setzen, Kommando"
2. Element mit "Was denn genau?" also z.B. "Geschwindigkeit setzen"
3. Element wo man die Einheit wählen kann in der man den gewünschten Wert spezifizieren möchte, z.B. "rpm, m/s, Promille"
4. Element zur Werteeingabe - je nach 2 und 3 als Kommazahl oder Ganzzahl.

Jetzt sollte das ganze natürlich idealerweise:
1. Sich anpassen so dass nur relevante Sachen angezeigt werden.
2. Falls es nur eine Einheit gibt, könnte man diese als label direkt hinter die Eingabe stellen. Ansonsten als Combobox?
3. Es gibt auch Sachen ohne Einhait, z.B. die Start/Stopp Befehle. Hier sollte dann auch keine Einhait und keine Werteeingabe erfolgen.
4. Aus dem ganze Gewäsch will ich dann aber natürlich wieder auf meinen struct kommen, den ich dann verschicken kann.

Ich habe ja schon ein paar Sachen mit Databinding gemacht, aber das stellt mich jetzt doch schon vor Probleme. Und die SelectedIndexChanged-Events mit case-Abfragen vollzustopfen finde ich jetzt irgendwie auch doof.

Also die Frage: Wie könnte man diese GUI elegant realisieren? Geht das mit Databinding?
Ach, ich könnte mir auch vorstellen zu WPF zu wechseln falls das dort wesentlich besser geht. Ich habe halt schon Erfahrung mit WinForms.
Christian S.
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 20451
Erhaltene Danke: 2264

Win 10
C# (VS 2019)
BeitragVerfasst: Fr 04.01.13 22:26 
Hm. Ich überlege, ob man das über ein Custom Attribute macht, welches man jedem Enum-Wert gibt. Das könnte dann Informationen wie z.B. eine Kategorie ("Request", "Response", "Kommando", ...), eine Einheit usw. enthalten, die man zum Bauen der GUI braucht.

Wenn man die Attribute ausliest, kann man mit ein bisschen LINQ recht fix alle verfügbaren Kategorien (erste Combobox) erhalten, dann die in der Kategorie enthaltenen Enums ("Was denn genau?"), usw.

Das ist sicherlich kein kleiner Aufwand, aber es dürfte recht flexibel sein.



Ach ja, wie geht denn 0x100 in ein Byte? :gruebel:

_________________
Zwei Worte werden Dir im Leben viele Türen öffnen - "ziehen" und "drücken".

Für diesen Beitrag haben gedankt: jfheins
jfheins Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 918
Erhaltene Danke: 158

Win 10
VS 2013, VS2015
BeitragVerfasst: Fr 04.01.13 23:09 
Danke dafür - das werde ich gleich morgen mal probieren. Dann bekommt jeder enum Wert eine Zeile voran, das geht noch. (Ich hatte schon befürchtet, für jeden Wert eine Klasse abzuleiten)

Und das mit den 0x100: das sind die neuen 10bit-Bytes - die sind jetzt ganz neu in 2013 :-P
Aber danke für den Hinweis, wir entfernen dann einfach überall die "0x" dann passt's ja wieder. Und komisch dass der Compiler nicht meckert...
jfheins Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 918
Erhaltene Danke: 158

Win 10
VS 2013, VS2015
BeitragVerfasst: Sa 05.01.13 21:49 
Sooo, ich habe es jetzt implementiert und es geht :)
Es war mir neu dass man einzelnen Werten eines enums noch Attribute zuweisen kann. Und das auslesen ist auch nicht ganz ohne.
Mir ist bewusst, dass mein Problem speziell ist, trotzdem noch hier meine Lösung auszugsweise:
Ich habe die Eingabe in "Kategorie", "Typ", "Wert" und "Einheit" unterteilt.
ausblenden volle Höhe C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
147:
148:
149:
150:
151:
152:
153:
154:
155:
156:
157:
158:
159:
    [AttributeUsage(AttributeTargets.Field)]
    public class TypeInfoAttribute : Attribute
    {
        public Category Category { get; private set; }
        public string DisplayName { get; private set; }
        public string Unit { get; set; }
        public string Data2Name { get; set; }
        public bool NeedsValue { get; set; }

        public TypeInfoAttribute()
        {
            Unit = String.Empty;
            Data2Name = String.Empty;
            NeedsValue = true;
        }

        public TypeInfoAttribute(Category cat, string displayName)
            : this()
        {
            Category = cat;
            DisplayName = displayName;
            NeedsValue = cat != Category.Get_Sensor
                            && cat != Category.Get_Variable
                            && cat != Category.Get_Variable;
        }

        public TypeInfoAttribute(Category cat, string displayName, string data2name)
            : this(cat, displayName)
        {
            Data2Name = data2name;
        }
    }

    public enum Category
    {
        [Description("Allgemein")]
        General,
        [Description("Sensor auslesen")]
        Get_Sensor,
        [Description("Variable auslesen")]
        Get_Variable,
        [Description("Konstante auslesen")]
        Get_Constant,
        [Description("Variable setzen")]
        Set_Variable,
        [Description("Konstante setzen")]
        Set_Constant,
        Response,
        [Description("Befehl")]
        Command,
        Raw
    }

    public enum COMM_Type : byte
    {
        // Allgemeine Datentypen
        [TypeInfo(Category.General, "Ping ausführen", NeedsValue = false)]
        PING = 00,
        ACK = 01,
        NACK = 02,

        // Sensorik
        [TypeInfo(Category.Get_Sensor, "Status")]
        REQ_STATUS = 10,
        [TypeInfo(Category.Get_Sensor, "Motor", Unit = "U/min")]
        REQ_MOTORSPEED = 11,
        [TypeInfo(Category.Get_Sensor, "Alle Sensoren")]
        REQ_SENSORDATA = 12,
        [TypeInfo(Category.Get_Sensor, "Gefahrene Strecke", Unit = "m")]
        REQ_DRIVEN_DISTANCE = 13,
        [TypeInfo(Category.Get_Sensor, "Bandsensor (binär)")]
        REQ_ROPE_SENSOR_VAL = 14,
        [TypeInfo(Category.Get_Sensor, "Bandsensor")]
        REQ_ROPE_SENSOR_STATE = 15,
        [TypeInfo(Category.Get_Sensor, "Temperatur""Sensor", Unit = "°C")]
        REQ_TEMPERATURE = 16,
        [TypeInfo(Category.Get_Sensor, "Entfernung oben", Unit = "m")]
        REQ_TOP_DISTANCE = 17,
        [TypeInfo(Category.Get_Sensor, "Entfernung unten", Unit = "m")]
        REQ_BOTTOM_DISTANCE = 18,

        // Variablen lesen
        [TypeInfo(Category.Get_Variable, "Zyklen")]
        REQ_CYCLES = 19,
// .... etc.
        // Kommandos
        [TypeInfo(Category.Command, "Start", NeedsValue = false)]
        CMD_START = 100,
        [TypeInfo(Category.Command, "Stopp", NeedsValue = false)]
        CMD_STOP = 101,
        [TypeInfo(Category.Command, "Fahren (manuell)", Unit = "m/s")]
        CMD_SETSPEED = 102,
        [TypeInfo(Category.Command, "Fahren (manuell)", Unit = "U/min")]
        CMD_SETSPEED_RPM = 103,
        [TypeInfo(Category.Command, "Fahren (manuell)", Unit = "‰")]
        CMD_SETSPEED_PROMILLE = 104,
        [TypeInfo(Category.Command, "Notaus", NeedsValue = false)]
        CMD_NOTAUS = 105,

        //RAW Payload zB für Debug strings
        [TypeInfo(Category.Raw, "Debugstring")]
        RAW_PAYLOAD = 200
    }

    public static class EnumHelper
    {
        public static TypeInfoAttribute GetTypeInfo(this COMM_Type enumeration)
        {
            return GetAttribute<TypeInfoAttribute>(enumeration);
        }

        /// <summary>
        /// Converts the <see cref="Enum" /> type to an <see cref="IList" /> 
        /// compatible object.
        /// </summary>
        /// <param name="type">The <see cref="Enum"/> type.</param>
        /// <returns>An <see cref="IList"/> containing the enumerated
        /// type value and description.</returns>
        public static IList<KeyValuePair<Enum, string>> ToList<T>(this Type type, Expression<Func<T, bool>> selector, Expression<Func<T, string>> display)
            where T : Attribute
        {
            if (type == null)
                throw new ArgumentNullException("type");

            var filter = selector.Compile();
            var displaystr = display.Compile();


            var list = new List<KeyValuePair<Enum, string>>();
            Array enumValues = Enum.GetValues(type);
            var items = new HashSet<string>();

            foreach (Enum value in enumValues)
            {
                T description = GetAttribute<T>(value);

                if (description != null)
                {
                    var str = displaystr(description);
                    if (!items.Contains(str) && filter(description))
                    {
                        list.Add(new KeyValuePair<Enum, string>(value, str));
                        items.Add(str);
                    }
                }
            }

            return list;
        }

        private static T GetAttribute<T>(Enum value)
        {
            T attribute = value.GetType()
                .GetMember(value.ToString())[0].GetCustomAttributes(typeof(T), false)
                .Cast<T>()
                .SingleOrDefault();
            return attribute;
        }
    }


Die generische Extension-Methode ist interessant zu proggen wenn man sowas zum ersten mal macht. Sie bekommt einen Typparameter (den Typ des Attributs) und den Tpyen des enums übergeben. Zusätzlich zwei Lambda-Ausdrücke: Den einen als filter damit ich mir nur bestimmt rausfiltern kann und den anderen um festzulegen, welche Property des Attributs denn nun angezeigt werden soll. Duplikate werden mit dem Hashset entfernt.

Die Methode wenn die Kategorie geändert wird sieht dann wie folgt aus:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
        private void category_cb_SelectedIndexChanged(object sender, EventArgs e)
        {
            var cat = (Category)category_cb.SelectedValue;
            type_cb.DisplayMember = "Value";
            type_cb.ValueMember = "Key";
            type_cb.DataSource = typeof(COMM_Type).ToList<TypeInfoAttribute>(x => x.Category == cat, x => x.DisplayName);
        }

Für diesen Beitrag haben gedankt: Christian S.