°Junoland.de HOME     www.Junoland.de     Valid HTML 4.01 Transitional     (c) Copyright 2003 °Andreas Klotz, Cologne, Germany [upd.Oct.2008]     AKL@Junoland.de


Excel VBA Kurs
Excel Exkursion in VBA
Programmieren lernen mit Excel VBA
ca.116 Seiten / 1,3 MB: 1 HTML-Textfile 0,2 MB + 60 Abbildungen 0,9 MB +
Zip-File 0,2 MB mit 9 Demos

Ablaufplan

 ↓ 

i. Intro & Def
1. Willkommen 2. Was ist VBA? 3. Was wird Sie erwarten?

A. IDE & Cell - Entwicklungsumgebung und Zellzugriff
1. VBA-Programmierbildschirm starten 2. wechseln zw. Excel und VBA 3. Steuerelemente-Toolbox einblenden
4. Schalter anlegen (Command-Button) 5. warum englische Ausdrücke? 6. Toolbox dauerhaft in Excel-Menü einbauen
7. Button-Eigenschaften einstellen 8. Hello World! Unsere ersten VBA-Programmzeilen schreiben 9. VBA-Editor-Einstellungen: Tabulator-Schrittweite und Fehlerbehandlung
10a. VBA-Bildschirm in 4 Fenster unterteilen
10b. +Form-Design-Fenster
11. Kommentier-Symbol in Menüleiste einbauen 12. Funktionstasten
F1, Strg+F, Strg+H, Shift+F2, Ctrl+Shift+F2, (Shift)+Tab
13. Zellzugriff: Range / Cells / Offset 14. Stufengliederung: Range / Sheet / Workbook / Application 15. Probleme bei nicht-aktiviertem Arbeitsblatt / Arbeitsmappe

 ↓     ↑ 

B. VAR & LOOP - Variablen, Schleifen, Wenn/Dann-Verzweigungen
1. Visual oder Manual? 2. Variablen 3. Speedtest: Variablen vs. Zellzugriff
4. warum Option Explicit (Variablendeklaration erzwingen) ? 5. Variablen-Typen 6. Dim / Public und über das Modul hinaus
7. Bedingungen 8. Schleifen 9. Sub & Function
10. Parameter-Übergabe ByRef& ByVal 11a. Inputbox : User-Eingaben
11b. If MsgBox : User-Entscheidung
12. Diverse VBA-Innereien
(Goto / Stop / Exit Sub / End / Call / _ / : )

+++    Übungen am praktischen Beispiel - Teil I    +++
1. zum Beispiel: Pizza-Fabrik Produktionsplan 2. Function FindProdNum 3. summieren nach Pizzatyp
4a. FindVorProd und mehr Funktionen
4d. Lösch Abschnitt
5. Tagesmengen und Umstellverlust ermitteln 6. Produktionsabschnittsmengen eintragen

 ↓     ↑ 

C. BUTTON & FORM - Buttons, Listen, Formularbau
1. Events - Ereignisse wie Button_click und Before_Close 2. Form - Formular für Toolbox-Objekte 3. Listbox
4. andere Toolbox-Objekte : Label, Textbox, Options 5. Objekt-Logik - Ablauf steuern durch ausblenden  

+++    Übungen am praktischen Beispiel - Teil II    +++
7. Ablaufsteuerung Pizza-Plan-Bearbeitung 8. Pizza-Plan: Eingabefelder erst nach Listbox-Wahl zeigen 9. Pizza-Plan: Kampagnenmenge bei Auswahl einer Sorte einblenden
10. Pizza-Plan: im newP-Schalter Form vorbereiten (Kampagnenlänge...) 11. Pizza-Plan: Tagesmengen einer Kampagne eintragen 12. Pizza-Plan: Hinweise zu weiteren Funktionen

 ↓     ↑ 

D. ERROR & SYSTEM Fehlerbehandlung und Systemfunktionen
1. Fehlermeldung! 2. Debug (zur Kontrolle schrittweise ablaufen lassen) 3. Debugger nicht vergessen auszuschalten!
4. Der Macrorecorder und seine Grenzen 5. Druck einrichten 6. Teilbereich in separate Datei exportieren
7. Quelltext (Sourcecode) verstecken 8. Get Network User Name (angemeldeten Benutzer ermitteln) 9. weitere Funktionen kurz dargestellt (Copy & Paste, Screenupdating etc.)

p. END & LINK
1. VBA sinnvoll einsetzen 2. Zukunft von VBA 3. zum Demo-Download
4. SourceCode pizza05_logic.xls 5. SourceCode pizza06_move.xls 6. Links zu Userforen


 ↑ 

 

 ↑  Intro


i1.   ↑    Willkommen im VBA-Grundkurs "Excel Exkursion" !

Dieser Kurs richtet sich an absolute Beginner - damit meine ich Menschen, die noch nie oder sehr wenig programmiert haben. Erwarten Sie von diesem schmalen Script kein Wunder. Es ist nur als knapper Einstieg gedacht, ohne Anspruch auf Vollständigkeit. Sie werden an vielleicht vier halben Tagen (oder Nächten) gerade einmal soviel erlernen, um entscheiden zu können, ob Sie tiefer in die VBA-Welt eindringen wollen. Ich hoffe aber, Ihnen über die ersten Einstiegshürden hinweghelfen und Ihren Appetit anzuregen zu können.

Nehmen Sie sich die Freiheit, kurz und gierig durch die Kapitel zu laufen und auch dann weiter zu blättern, wenn Sie das eine oder andere nicht verstanden haben. Sie müssen natürlich auch nicht jedes kleine Beispiel bis zur letzten Zeile akribisch nacharbeiten; Sie können ja jederzeit auf vorangegangenen Stoff zurückspringen, wenn Ihnn ewas nur noch nebelhaft in Erinnerung sein sollte. Das wichtigste ist, dass Sie es selber ausprobieren und dazu die Geduld aufbringen, Ihren Weg durch die vielen VBA-Fenster und Projektteile zu finden. Ich werde Ihnen hierbei helfen, so gut ich kann!



i2.   ↑    Was ist VBA?

VBA steht für Visual Basic for Applications.

Was bitte heißt das? - Visual Basic (VB) ist die populärste PC-Programmiersprache für den interessierten Laien und für kleine bis mittlere Unternehmensanwendungen. (C++ und Java sind die wichtigsten im Profi-Bereich). VBA ist ähnlich aufgebaut wie VB, mit dem Unterschied, dass man mit VBA zusätzlich unmittelbar auf das Verhalten von bestimmten Microsoft-Büroprogrammen (Applications) einwirken kann; es gibt VBA nicht nur für Excel, sondern auch für Word, Access und Powerpoint. Wenn man sich für Programmierung zu interessieren beginnt, kann man mit Excel-VBA auf schnellstem Weg zu Resultaten kommen, denn das Spielfeld für das Sichtbarmachen von Zahlen, Wörtern, Bildern und Tönen ist schon durch das Schachbrett-artige Excel-Raster-Blatt schon vorbereitet.

Excel selbst wird treffend als "Schweizer Offizierstaschenmesser" bezeichnet - sehr universal einsetzbar, aber für größere Datenmengen und viele gleichzeitige Zugriffe ungeeignet.

Alles in allem gibt es zwischen VBA und VB keinen nennenswerten Unterschied. VB wird wegen seinem freundlichen Einstiegsansatz von vielen selbsternannten Fachleuten als Spielzeug abgetan. In der Tat, VB (und VBA) kann einfach sein - kann aber genauso so komplex sein wie 'ausgewachsene' Profisprachen, wenn man z.B. Objekte anlegt, um auf fremde Bibliotheken zuzugreifen.

Zu ähnlich schnellen Ergebnissen kommt man allenfalls noch in der Homepage-Programmierung (HTML mit Javascript und serverseitigen Scriptsprachen, wobei dort aber engere Grenzen gesetzt werden durch die Rollen-Verteilung zwischen Browser und Server und den sich daraus ergebenden Sicherheitskonzepten).



i3.   ↑    Was wird Sie in den nächsten Stunden erwarten?


Sie werden lernen, wie man zum Programmier-Modus wechselt. Dort werden einige VBA-Ansichtsfenster erklärt. Den Zugriff auf Excel-Zelle und -Arbeitsblatt erkläre ich so knapp wie möglich. Ein erstes simples Beispiel folgt (Hello-World-Meldung, dreimal wiederholt mit Zähler auf Excel-sheet).
Die wichtigsten Schleifen, Bedingungen, Variablen und Programmsegmente werden mit kurzen Beispielen besprochen. An einer größeren Beispielsaufgabe, der Produktionsplanung für eine Pizza-Fabrik, erproben wir dann unsere neuen Kenntnisse und Fertigkeiten.
Wir bleiben bei dem Produktionsplanbeispiel, um den Umgang mit sichtbaren Objekten wie Formularen, Listen und Eingabefelder zu erlernen, in dem wir alles das, was wir zur komfortablen Eingabe von neuen Produktionsabschnitten brauchen, anlegen und mit unseren bisherigen Routinen verknüpfen.
Hier zeige ich Ihnen Instrumente zur Fehlerbehandlung und noch einige Besonderheiten an Excel-VBA, wie zB dem Verfahren, Zellbereiche zu verschieben, Ausdrucke einzurichten und Zellbereiche als Extrakt in eine separate Datei zu schreiben.

Zu guter Letzt möchte ich Ihnen - aus meiner persönlichen Sicht - einige Hinweise zum sinnvollen Einsatz von Excel-VBA geben und einen kurzen Ausblick auf weitere Entwicklungen.


 ↑  A. IDE & Cell - Entwicklungsumgebung und Zellzugriff

Jetzt kommen einige lästige Einstellungsübungen auf Sie zu, die wir gleich zu Anfang einschleifen sollten, um dann anschließend mit der Programmierung zu beginnen . Ohne Anleitung würden Sie wahrscheinlich deutlich mehr als eine Stunde brauchen, um all diese Fenster und Menüpunkte zu finden.

Wenn Sie nun zu der Meinung kommen sollten, dass dies nicht für die Benutzerfreundlichkeit dieser Entwicklungsumgebung (IDE) spricht, schauen Sie sich einmal vergleichbare Produkte an. Ich denke, dass alles in allem die hier vorliegende VBA-IDE eine der besten Entwicklungsumgebungen überhaupt ist - auch wenn ich mich hin und wieder über einige Ungereimtheiten ärgere :-(

A1.   ↑    VBA-Programmierbildschirm starten

Excel starten und das gewohnte Bild mit dem leeren Arbeitsblatt "Mappe1.xls" erscheint:

sheet-screen

mit [Alt] + [F11] kann man den Programmierbildschirm (VBA-Modus) zum ersten Mal hinzuschalten:
(gut versteckt, oder?)

vba-screen

A2.   ↑    und hier kann man zwischen Excel- und VBA-Bildschirm wechseln ...

switch-excel/vba

( Achtung! Reagiert nicht immer sofort - evtl. mehrmals probieren! )
A3.   ↑    Steuerelemente-Toolbox einblenden

Nun legen wir einen ersten Schalter an, von dem aus unser künftiges VBA-Programm ablaufen soll.

Hierzu müssen wir die Werkzeugkiste zum Erzeugen und Bearbeiten von Schaltern, Listenfeldern, Formularen und anderen Steuerelementen anwählen - ExcelMenü: Ansicht / Symbolleisten / SteuerelementToolbox:

toolbox-select

A4.   ↑    Schalter anlegen (Command-Button)

Aus all den angebotenen Objekttypen wählen wir "Befehlsschaltfläche", indem wir einfach einmal kurz mit der linken Maustaste auf das entsprechende Symbol excel_toolbox_button.png klicken; dabei können wir feststellen, dass das erste Symbol "Entwurfmodus" excel_toolbox_designmode.png automatisch mit-aktiviert wird. Wir halten die Maustaste also nicht fest, sondern fahren in einem zweiten Schritt mit unserem Mauszeiger an irgendeine beliebige Stelle unseres Excel-Arbeitsblatts. Dort drücken wir die linke Maustaste so lange nieder und verschieben dabei gleichzeitig den Mauszeiger, bis wir ein Rechteck in gewünschter Größe markiert haben. Wenn wir nun die Maustaste loslassen, haben wir eine "Befehlsschaltfläche" im typischen Windows-Grau erzeugt, die ich aber künftig kurz und englisch "Button" nennen werde.

set button

A5.   ↑    'mal zwischendurch bemerkt: VBA-Ausdrücke werden englisch geschrieben

Da wir seit den letzten VBA-Versionen Befehlszeilen grundsätzlich in englischer Sprache schreiben müssen (eine spezielle deutsche VBA-Version wird seit Excel 5 nicht mehr angeboten), werde ich auch die Elemente englisch bezeichnen, die auf Excel-Arbeitsblatt-Ebene eine deutsche Übersetzung haben. Denn wenn wir auf diese Elemente auf VBA-Ebene Bezug nehmen, müssen wir sie wieder - zum Leidwesen vieler Anwender - in der englischen Entsprechung bezeichnen. Es nützt aber nichts, Objektbezeichnungen ständig von deutsch auf englisch hin und her zu übersetzen, denn je länger man sich mit VBA-Programmierung befasst, um so mehr wird man von der Excel-Arbeitsblatt-Oberfläche ("visual") wegtreten und manuell Programmzeilen schreiben im VBA-Englisch. Doch keine Angst, es handelt sich um vielleicht 2-3 Dutzend kurze Basic-Grundbegriffe zur Schleifenbildung und Verzweigung wie Loop und Exit, die man so oft anwenden wird, dass sie einem schnell vertraut werden. Dazu kommen dann umfangreichere Objektbezeichnungen, die aber grundsätzlich aus Auswahllisten übernommen werden können, so dass man sein Gedächtnis nicht all zu sehr bemühen muss, vor allem, was die korrekte Schreibweise anbelangt. Und eingedeutscht wären diese Begriffe nicht unbedingt klarer als im Original.

Das VBA-Englisch hat ganz entscheidende Vorteile:
Ein Ausdruck wie "Toolbox" ist kürzer als "Werkzeugkiste", "Befehlsschaltfläche" ist wesentlich umständlicher als "Button", "worksheets" entspricht "Arbeitsblätter" und "Eingabeaufforderungstaste" ist natürlich um einiges länger übersetzt als das knappe "Enter-Key". Wir werden sehr bald mit komplexen Ausdrücken zur Bezeichnung der Excel- und Windows-Objekte arbeiten, die schon in englischer Sprache lang genug ausfallen, z.B.:
Application.ActiveWorkbook.Sheets.Add um ein neues Arbeitsblatt 'sheet' anzulegen. Ins Deutsche übersetzt würde das den Rahmen des Erträglichen endgültig sprengen!

Ein weiterer Vorteil an dem Englischen ist, dass man seinen VBA-Code weltweit austauschen kann.

A6.   ↑    Toolbox dauerhaft in Excel-Menü einbauen

Bevor wir uns nun unserem neuen Schalter zuwenden, haben wir noch etwas einzustellen. Da wir die Toolbox häufiger brauchen werden, wäre es gut, wenn wir diese dauerhaft in das Excel-Menü einbauten. Dazu gehen wir mit dem Mauszeiger auf die blaue Titelzeile "Steuerelemente-Toolbox" und drücken die linke Maustaste nieder und halten sie solange fest, bis wir die Toolbox erfolgreich an eine freie Stelle des Excel-Hauptmenüs verschoben haben:

toolbox-moving

So ungefähr sollte die Werkzeugkiste nun im Excel-Hauptmenü eingebaut sein
(Um diese Sektion wieder woanders anzuordnen, Mauszeiger an linken Rand positionieren excel_toolbox_psoitioner.png und linke Maustaste gedrückt halten zum Verschieben)
:

toolbox-moved

A7.   ↑    Button-Eigenschaften einstellen

Wir vergewissern uns, dass der Entwurfmodus immer noch eingeschaltet ist excel_toolbox_designmode.png, verschieben gegebenenfalls den Button mit niedergedrückter linker Maustaste an einen beliebigen anderen Ort im Excel-Sheet und klicken unseren neuen Button mit der rechten Maustaste an, um einige Eigenschaften zu verändern - dabei bleiben wir immer noch auf der Excel-Arbeitsblatt-Oberfläche, also noch nicht auf dem VBA-Bildschirm
(Eigenschaften von allen möglichen Objekten werden traditionsgemäß mit der rechten Maustaste per Kontextmenü eingestellt)
:

button-properties

Jetzt erschrecken Sie bitte nicht, wenn daraufhin ein üppiges Auswahl-Menü aufspringt! Wir werden davon nur die allerwenigsten Eigenschaften einstellen. Fast alle Default-Werte (Vorgabewerte) kann man erst einmal so hinnehmen wie vorgefunden.

Wir verändern den Defaultwert "CommandButton1" der Name-Eingenschaft zu "cmdFangAn" und die Aufschrift (merkwürdigerweise "Caption" genannt) unseres Buttons von "CommandButton1" zu "Fang an !".

A8.   ↑    Hello World! Unsere ersten VBA-Programmzeilen schreiben

excel_buttontovba Mit einem beherzten Doppeklick auf unseren Button (Entwurfmodus sollte immer noch eingeschaltet bleiben) stoßen wir nun zum ersten Mal das Tor zur VBA-Programmierung auf!


Wir sollten das mickrige Fester mit dem VBA-Sourcecode (Quelltext, Programmbefehlstext) zu einer seiner künftigen Bedeutung angemesseneren Größe bringen:

vba-screen-tomax

nun schreiben wir unsere erste VBA-Befehlszeile ein, und zwar zwischen
Private Sub cmdFangan_Click()
und
End Sub



Private Sub cmdFangan_Click()
  MsgBox "Hello World!"
End Sub	

Schnell wechseln wir vom VBA-Bildschirm zum Excel-Sheet, deaktivieren dort den Entwurfmodus und klicken auf unseren Button "auf-auf !"- und erhalten als Antwort diese klassische Meldung:

helloworld



Ich hoffe doch, dass ich den Wechsel zwischen VBA-Bildschirm und Excel-Arbeitsblatt hinreichend erklärt habe - wenn nicht, schauen Sie bitte noch einmal hier hinein.

Und um unsere Freude an diesem beispielhaften Programm zu steigern, bauen wir noch einen Rücksprung ein, der unseren Menschheitsgruß wiederholen soll:

Private Sub cmdFangan_Click()
[A5] = 0

hierhin: 
  [A5] = [A5] + 1
  MsgBox "Hello World! "
Goto hierhin

MsgBox "Fertig, Mama!"
End Sub


Bitte nehmen Sie mir diesen ersten primitiven Schleifenbau nicht übel - wir werden bald schon zu eleganteren Lösungen kommen.

und wieder starten wir unser Programm ...

run (das können Sie übrigen auch ganz einfach aus dem VBA-Programm-Code aus, indem Sie den Sub/UserForm-Ausführen-Pfeil hier anklicken oder die F5-Taste drücken)


... und stoßen auf ein ernstes Problem:

gotoforever


So-so, die Abbruchbedingung fehlt also ...

Wenn wir nun also [Strg] + [Pause]-Taste (auf der Tastatur ganz rechts oben ) erfolgreich gedrückt haben, werden wir auf diese Meldung hier stoßen ...

debugorend


... die wir erst einmal mit [Beenden] quittieren sollten.

3 typische Fehler beim Umgang mit Excel-VBA


Achtung! typische Anfänger-Fehler beim Umgang mit der Excel-VBA-Entwicklungsumgebung sind:
A9.   ↑    VBA-Editor-Einstellungen: Tabulator-Schrittweite und Fehlerbehandlung

Leider muss ich Sie jetzt noch einmal für 1-2 Minuten stören, damit Sie einige Einstellungen checken, um Ihre Entwicklungsumgebung zu optimieren; danach wird nicht mehr allzuviel "Gefummel" auf Sie zu kommen. Bitte speichern Sie das Excel-File mit dem Hello-World-Beispiel und alles andere irgendwohin ab, wo Sie es später wiederfinden werden.

Um Optionen einstellen zu können, muss irgendein Excel-Arbeitsblatt vorliegen, und wenn es einfach nur eine neue leere Arbeitsmappe ist.

Extras | Optionen | Editor:
   [x] Variablendeklaration erforderlich
(dies ist wichtig, um uns von Anfang an zu einer übersichtlichen Variablendeklaration aufzuraffen)
Tab-Schrittweite [ 2 ]
(sonst entstehen bei verschachtelten Ausdrücken mit sprechenden Variablenbezeichnungen Zeilen, die sehr schnell über den rchten Bildschirmrand gehen)

Extras | Optionen | Allgemein:
   Fehlerbehandlung :
   ( o ) Bei nicht verarbeiteten Fehlern unterbrechen
(sonst stoppen unsere VBA-Programme z.B. bei jeder nicht-aufgefunden Datei, auch wenn wir dafür eigens eine Fehlerbehandlungsroutine geschrieben haben)

vba-settings

Und nun sollten Sie Excel bitte komplett beenden (PC nicht 'runterfahren) und wieder neu starten, damit die Einstellungen wirksam werden. Starten Sie Excel erneut, schalten Sie wieder mit [Alt] + [F11] Taste zu VBA und dort machen Sie bitte im Project-Fenter (VBA-screen oben links) einen Doppelklick auf "Tabelle1" - Sie sollten dort in der ersten Zeile lesen können:
Option Explicit
also die Anweisung, die besagt, dass Sie sich dazu entschlossen haben, Variablen künftig vor Gebrauch anzumelden.

A10a.   ↑    VBA-Bildschirm in 4 Fenster unterteilen


Sorgen Sie bitte dafür, dass Ihr VBA-Bildschirm eine Basis-Einteilung in 4 Fenstern erfährt. Hierzu schalten Sie ggf. folgendes aus dem Pulldownmenü von Excel-VBA ein:

So ähnlich wie in folgender Abbildung, sollte der VBA-Bildschirm dann aussehen:

vba-views

Oben links wird das Project angezeigt in all seinen Untergliederungen von Sheet-Modulen, Form-Modulen und zusätzlichen Modulen, auf die wir bei Bedarf noch zurückkommen werden. Wechsel zu anderen Modulen per Doppeklick (einfach-Klick zeigt immer noch den VBA-Code des vorherigen Moduls).

Vorsicht ist dann geboten, wenn Sie gleich mehrere Excel-Files auf einmal geöffnet haben! Sie sollten aufpassen, dass Sie nicht versehentlich Änderungen für das falsche Project (File) vornehmen. Ich rate Ihnen also, nur dann mit mehreren Files zugleich umzugehen, wenn es sich nicht vermeiden lässt.

Es kann sein, dass Sie Ihre Module ähnlich wie der Windows-Explorer nicht in aufgeklappter Baum-Darstellung vorfinden. Machen Sie also gegebenenfalls einen Doppelklick auf das Project (also auf Ihr Excel-File) , auf "Microsoft-Excel-Objekte", worunter Arbeitsmappen- und Sheets-Module vorzufinden sind, und auf Formulare und Module.

Unten links werden Eigenschaften (properties) des jeweils angewählten Projektteils angezeigt. Hier kann man z.B die interne Bezeichnung von Sheets ändern, Eigenschaften von hinzugefügten Formularen, etc.

Oben rechts ist der eigentliche VBA-SourceCode. Der Quelltext ist entweder dem ganzen File zugeordnet (z.B. für die Dinge, die beim Öffnen eines Files geschehen sollen), oder einem einzelnen Button eines Sheets oder einem separat hinzugefügten Formular (das ist eine Art Sammelbehälter für verschiedene Objekte wie Buttons, Eingabefelder, Auswahl-Listen) oder einem beliebig hinzugefügten allgemeinen Programm-Moduls.
(Über all diese Punkte wird noch gesprochen werden)

Unten rechts das Debug-Fenster - auch Direct-Fenster genannt - zum schrittweisen Ablauf des Programms; zwischen den einzelnen Schritten können aktuelle Variablen- oder Zell-Inhalte zur Kontrolle angezeigt und sogar verändert werden.

A10b.   ↑    +Form-Design-Fenster

An derselben Stelle, an der das Code-Fenster für unsere jeweiligen Module steht (Arbeitsblatt-Modul, freies Modul), erscheint für Forms das FormDesign-Fenster. Ein Doppelklick auf eine Userform im Projektfenster (im VBA-screen links oben) bringt also zunächst das Form-Objekt in Grundriss-Darstellung, an dem man mit der Maus Objekte verschieben, vergößern, etc. kann:

formdesignsolo

Um dies zu erfahren, sollten Sie jetzt gleich einmal ein Formular anlegen, falls Sie noch keines vorliegen haben, in dem Sie mit rechter Maustaste irgendwo ins Project-Fenster klicken und "Einfügen Userform" ausführen. Userforms und andere Module können übrigens exportiert und importiert werden. Man könnte sich also die wichtigsten Befehle, die man immer weieder verwendet in einem separates Modul führen, das man dann in neue Projekte importiert.

Man kommt im Laufe der Entwicklungsarbeit viel öfter zum Form-Design-Fenster, als einem vielleicht recht ist! Man wird z.B. aus jeder abgebrochenen Debugger-Session dorthin zurückgeworfen statt zum VBA-Code, den man doch wahrscheinlich bearbeiten möchte!

formdesignmode2

Um nun an den Sourcecode eines Form-Objekts zu gelangen, klickt man per rechter Maustaste auf das entsprechende Form-Modul im Project-Fenster und wählt "Code anzeigen" aus dem Kontext-Menü; und dann kann man weiter per Ctrl+Shift+F2 (siehe Hotkeys) zu seinen letzten Text-Eingaben zurückkehren ... - ärgerlich ? - In meinen Augen schon!

excel_formcode.png

A11.   ↑    Kommentier-Symbol in Menüleiste einbauen

Man kann bequem ganze markierte Blöcke von Befehlszeilen deaktivieren, indem man das Hochkomma als Kommentar-Zeichen voranstellt, z.B so hier:

Sub test()
Dim a, b
a = 123
b = 456 
'If a = 1 then 
'  b = 7 * a
'End If	
End Sub


Und weil man das wohl ziemlich oft machen wird, ist es gut, wenn man den entsprechenden Befehl (Block auskommentieren) fest in die Pulldown-Menü-Leiste des VBA-Screens einbaut:
Rechts-Click auf die Pulldown-Menü-Leiste, Anpassen, Befehle, Bearbeiten, "BlockAuskommentieren" wählen, mit niedergedrückter linker Maustaste in die Menüleiste ziehen (ganz links anfassen); das gleiche auch mit BlockKommentierenAufheben" - fertig. menubar_a
menubar_b menubar_c
A12a.   ↑    Funktionstasten
F1 / Strg+F / Strg+H / Shift+F2 / Ctrl+Shift+F2 / (Shift)+Tab

Die von mir häufigst verwendeten Hotkeys (Schnellkombinationstasten):


12b.   ↑    'mal zwischendurch bemerkt: Ohne Maus kann man einfach schneller Programmzeilen schreiben
Versuchen Sie es auch einmal ohne Maus! Ich kenne überhaupt niemanden, der gut und schnell programmiert und nicht mehr oder weniger Tastatur-orientiert arbeitet! Der ständige Wechsel zwischen Tastatur und Maus beginnt nämlich nach einer Weile lästig zu werden, so dass es nicht schlecht wäre, wenn Sie sich wenigstens 5 Hotkeys merken würden:

Dass man sich mit [Pos1] an den Zeilenanfang stellen kann,und dann einfach nur die [Enter]-Taste drücken muss, um darüber eine Zeile einzufügen, brauche ich Ihnen ja wahrscheinlich nicht noch extra zu sagen. Eine Zeile darunter einfügen: mit [Ende]-Taste ans Zeilenende springen und [Enter].

Ganze Zeilen markiere ich mit [Shift] + Pfeiltaste nach unten / rechts usw. Den Cursor positioniere ich sonst aber mit der Maus, auch wenn es möglich wäre, diesen mit Pfeiltasten dorthin zu bewegen.

Diese Hotkeys werden Sie in fast allen Programmen dieser Art genau so wiederverwenden können, ja selbst in Betriebssystemtools wie dem Windows-Explorer, um zB einen langen Dateinamen in die Zwischenablage zu kopieren.

VBA macht übrigens glücklicherwise keinen Unterschied zw. Groß- und Klein-Schreibung. Tippen Sie alles bequem in kleinen Buchstaben ein und lassen Sie dann vom VBA-Editor alles, was an Befehlen und Variablen großgeschrieben wird, entsprechend in Großbuchstaben umwandeln. Bleibt alles klein, sehen Sie auf den ersten Blick, dass hier wohl ein Tipp-Fehler vorliegen muss.

A13.   ↑    Zellzugriff: Range / Cells / Offset


Es gibt eine verwirrende Vielfalt von Zugriffsmöglichkeiten auf die Zellbereiche der Excel-Arbeitsblätter, die uns aber schnell geläufig werden sollten:

A13a.    ↑    Range

Range bedeutet in Excel Zellbereich, mit zB: Range("C8")=123 wird der Zahlenwert 123 in die Zelle C8 eingetragen, mit a = Range("C8") wird der Zellinhalt von C8 in die Variable a eingelesen (kopiert).

Ein Zellbereich kann aus einem rechteckig angeordneten Verbund von benachbarten Zellen bestehen. Ebenfall mit Range , Range("C8:F15") , kann man auf diesen zugreifen.

Noch viel kürzer ist folgende Schreibweise:
[C8], was dem Range("C8") entspricht und auch fast immer zu empfehlen ist, um sich lästige Tipparbeit zu sparen. Für den Anfänger hat diese Schreibweise allerdings den Nachteil, dass hierzu keine Autocompletion angeboten wird; damit meine ich die Liste, die eingeblendet wird, wenn ich zB schreibe Range("C8"). ; sobald ich den Punkt eingeben habe, springt eine Liste der verfügbaren Erweiterungen für den betreffenden Ausdruck auf, die in grau-unterlegte Ausdrücke für Eigenschaften und gelb-unterlegte Ausdrücke für Aktionen unterschieden wird; tippe ich den Anfangsbuchstaben an (zB "C"), werde ich schnell an den gewünschten Ausdruck geführt ("ClearContents"), den ich dann nur noch mit der Tabulator-Taste (das ist die Taste links neben der [Q]-Taste) bestätigen muss. So kann man sich den Luxus von lesbaren unabgekürzten Ausdrücken leisten, ohne sich die Finger wund zu tippen.

Man kann Range noch um den Blattnamen erweitern, vor allem wenn man von einem bestimmten Sheet aus ein Makro gestartet hat, das auf ein anderes sheet zugreifen soll: Range("Plan!C8:F15").Clearcontents löscht zB den Bereich C8 bis F15 auf dem Arbeitsblatt 'Plan'.

Auch Bereichsnamen, die man im Arbeitsblatt vergibt, kann man ganz normal mit Range aufrufen: Range("Einkaufsliste").Copy kopiert zB den gleichnamigen Bereich, usw. ....

A13b.    ↑    Cells

Was ich im "normalen" Excel (also nur Zellformeln ohne VBA-Programmierung) schmerzlich vermisse, sind relative Zellbereiche; ich möchte z.B. etwas summieren, für das sich die Bereichsgrenze dynamisch einstellen lassen müsste - wenn es nur ginge! z.B. soll der Bereich(A1:A30) im April eben diese 30 Zellen umfassen, weil man diesen ausschneiden möchte oder eine Summe bilden will oder sonstiges. Im Mai mit seinen 31 Tagen wäre dann ja schon eine andere Formel nötig, es sei denn man könnte die Zellformel so schreiben, zB: Summe(A1:Tage), wobei Tage ein Zellbereich wäre, in dem die Monatstage eingetragen oder ausgerechnet wären... - das geht aber nicht!

Mit VBA ist es hingegen ein Leichtes, eine Adresse flexibel aus anderen Variablen zu bilden:
Range(Cells(1, 1), Cells(x, 1)) wäre hier die Lösung.
Cells(1,1) ist eine einzelne Adresse A1 (auch wenn dieses mit einem Wort im Plural benannt wird) und Cells(x, 1) ist ebenfalls eine einzelne Adresse, wobei diese je nach dem Wert von x variiert. Wenn zB x=30 ist, dann ist dies A30; wenn es 31 ist, dann ergibt der Ausdruck eben A31 usw. Ein Range aus beiden Zellen ist nichts weiteres als eben der gewünschte Ausdruck: A1 bis A-irgendwas, was bei einem x-Wert von 28 eben A1:A28 (also A1 bis A28) ergäbe und für x=31 eben A1:A31.

verkehrte Welt: Range("A5") = Cells(5,1)

Dem aufmerksamen Leser ist wahrscheinlich nicht entgangen, dass dem VBA-Ausdruck Cells nicht eine Bezeichnungsreihenfolge wie bei einem Schachbrett oder dem Excel-sheet zugrunde liegt, bei dem ja zuerst der Buchstabe um die Spalte (Column) zu bezeichnen angegeben wird und dann die Zahl um die Zeile (Row) zu benennen; sondern hier wird - leider - die gewohnte Reihenfolge vertauscht: Erst die Zeile, dann die Spalte, also zB D7 wird durch Cells(7,4) bezeichnet und nicht etwa durch Cells(4,7)!

A13c.    ↑    Offset


Man kann eine Kombination aus einer festen Zelladresse wie zB Range("Startpunkt") mit einem variablen Ausdruck ähnlich wie Cells flexibilisieren, zB:
Range("Startpunkt").Offset(0, 2) = 123
- was also bedeutet 2 Zellen rechts neben Startpunkt. Dies wird ähnlich wie Cells sehr häufig in Verbindung mit Schleifen gebraucht, um durch eine Liste von Zellen zu gehen, zB so hier:
While Range("sorte1").Offset(y, 0) <> was And y < maxProds
  y = y + 1
Wend
A14.   ↑    14.Stufengliederung: Range / Sheet / Workbook / Application

Das Arbeitsblatt kann so angesprochen werden:

Worksheets(1) oder mit Namensnennung Worksheets("Plan")
oder kürzer so:
Sheets(1) oder mit Namensnennung Sheets("Plan").

Warum verwendet man mühsam den Namen statt der kurzen Ordnungszahl? - Man kann ein Blatt vor ein anders verschieben, oder ein neues einfügen, so dass die hinteren um 1 Nummer höher "rutschen"; ein Blatt umbenennen tut man dagegen zwar auch hin und wieder einmal, aber der Interpreter "schreit" dann laut auf, wenn er dann dieses Objekt nicht mehr findet, während er bei der Nummerbezeichnung häufig etwas auf dem benachbarten Blatt duchführt, ohne dass man es sofort bemerkt. Und dann ist es oft schon zu spät, weil Eingaben und ZellFormeln brutal überschrieben wurden....

Man kann den zusammengesetzten Ausduck aus Sheet und Zelle u.a. auch so bilden:
Sheets("plan").Cells(3, 5).

A15a.   ↑    Probleme bei nicht-aktiviertem Arbeitsblatt / Arbeitsmappe

Den folgenden Abschnitt sollten Sie als Anfänger nur einmal kurz überfliegen und erst dann gründlicher zu Gemüte führen, wenn Sie mit mehreren Sheets und mehreren Excel-Files zugleich arbeiten und auf entsprechende Schwierigkeiten stoßen sollten.

Folgender elend-lange Ausdruck ist die vollständige Bezeichnung für eine bestimmte Zelle:
Application.Workbooks("Umsatz.Xls").Worksheets("Plan").Cells(8, 3)
Puh...! - Was bedeutet das im Einzelnen? Mit Application ist Excel als die Wurzel gemeint (im Gegensatz zu zB "Word" oder "Access"); mit Workbooks(2) ist das 2. geöffnete Excel-File gemeint (im Hauptmenü unter "Fenster" werden ja zueilen mehrere aufgelistet). Den Rest kennen wir schon: mit Worksheets ist die Gruppe von Arbeitsblättern eines Files gemeint, von denen hier das 1. gemeint ist; und Cells bezichnet davon eine bestimmte Zelle (C8).

Heißt das nun, dass man immer solche monströsen Ausdrücke schreiben muss, nur um eine winzige Zelle anzusprechen? - Natürlich nicht! Man verwendet Ausdrücke stufenweise in dem Grad vollständiger, in dem man von der jetzt aktiven Umgebung auf eine benachbarte Umgebung greifen möchte. Im Normal-Fall sagt man schlicht Cells(8, 3) und nichts weiter, wenn man eben den Programmcode auf demselben Workbook gestartet hat und sich immer nur auf einem bestimmten Sheet befindet...

Doch Vorsicht! Oft kommt es dann doch dazu, dass man Bereichsgrenzen überschreitet ohne daran zu denken! Wenn man zB ein Excel-Diagramm anklickt, dann steht der Zellzeiger nicht mehr auf dem Arbeitsblatt, das man immer noch glaubt vor sich zu haben - Buttons funktionieren dann nicht mehr! Man muss also dann wieder das Arbeitsblatt aktivieren: Sheets("Plan").Activate und irgendeine Zelle anwählen, damit der VBA-Interpreter endlich wieder "Boden unter den Füßen hat" : Range("K7").Select.

A15b.   ↑    Loosing Control
Wenn man langwierige Prozesse programmiert, die mehrmals gestoppt werden und dann wieder fortgesetzt werden, kann es vorkommen, dass Excel-VBA scheinbar die Orientierung verliert. Das Ärgerliche dabei ist, dass man dies nicht immer sogleich als Fehler um die Ohren gehauen bekommt, sondern in vielen Fällen nur ab und zu einmal.

In einem solchen Fall, wenn man also schon seit einer halben Stunde nach Fehlern sucht, aber nichts feststellen kann und die magere Meldung "Objektzugriffsfehler Nr. 4321" einem in ihrer Dürftigkeit wie Hohn vorkommt - dann sollte man es einmal mit einer vollständigeren Bezeichnung der Zellbereiche versuchen, indem man Schritt für Schritt untersucht, ab wo der Fehler auftritt und welche Grundlagen bis dahin vielleicht unklar geworden sind. Oft ist es ein Sheet, das ausdrücklich vor dem Zellzugriff noch einmal aktiviert werden muss. Das Aktivieren von einem Sheet (code Sheets("Tabelle1").Activate) reicht meistens nicht aus. Oft muss auch der Zellzeiger irgendwohin positioniert werden, damit VBA sich auf diese neuen Bereichsgrundlage einlässt ([A1].Select).


Wenn man über mehrere Files geht - solte man sich vor Augen halten, dass man ja immer nur VBA-Code aus einem einzigen File abarbeiten kann. Das Workbook, von dem der VBA-Code aus gestartet wird, muss man nicht namentlich ansprechen (Workbooks("Pizzaplan7.xls"), sondern es genügt, wenn man dann sagt: ThisWorkbook, zB ThisWorkbook.Close.

Davon zu unterscheiden ist das Workbook, das man gerade aktiviert hat, um dort etwas zu machen: AvtiveWorkbook.

Diese Begriffe sind aber einem Anfänger eigentlich nicht zumutbar; versuchen Sie, sich erst dann damit auseinanderzusetzen, wenn Sie in den Fall von bereichsübergreifenden Zugriffen auf Sheets und Workbooks kommen.
 

 ↑  B. Var & Loop - Variablen, Schleifen, Wenn/Dann-Verzweigungen

B1.   ↑    Visual vs. Manual

Sie werden die Maus künftig öfter aus der Hand legen als Ihnen vielleicht lieb ist!

Lassen Sie sich nicht von dem werbeträchtigen Zauberwort "Visual" in VBA zu der Annahme verleiten, dass Sie mit ein paar Mausklicks schon den fertigen Programmablauf erzeugen können; als wenn Sie nur in eine Kiste mit Bauklötzchen greifen müssten um einige vorgefertigte Elemente auszuwählen, einige geschmackliche Änderungen mit Drag and Drop einzustellen um dann in Windeseile ein fertiges Programm abzuliefern ... - Weit gefehlt!

Dass wir das Sourcecode-Fenster schon einmal größer eingestellt haben, hat durchaus seine Berechtigung. Denn wenn Sie tatsächlich "Blut lecken", also Gefallen an der VBA-Programmierung finden sollten, werden Sie im Laufe der Zeit feststellen, dass Sie ca. 60 Prozent Ihrer Aufmerksamkeit dem manuellen Eintippen von nüchternen und den Anfänger zunächt eher bizarr anmutenden Befehlswörtern wie "if err.number>0 then..." verbringen werden. Mehrfach ineinander-geschachtelte Klammerausdrücke, die die Zeilenbreite überschreiten, die verwirrende Vielfalt von Orten, an denen Programmtext und Objekteinstellungen vorgenommen werden können, die Tücken von Ereignisbehandlung und Formulareinstellungen, die Mühe, Eingabefehler abzufangen und den User mit entsprechenden Meldungen zu versorgen - all das wird den/die eine(n) oder andere(n) abschrecken!

Jetzt holen Sie einmal tief Luft und schauen Sie sich einmal ein typisches Beispiel für VBA-Programmierung im "fortgeschrittenen Stadium" an:

Function CheckInput() As Boolean
Dim RoCheck As Long, Co As Long, Cel As String
Dim par1 As String, par2 As String, a As Long
Dim LRefZeile As Long, okay As Boolean
Dim icel, yy As Long, oldlen As Long, maxlen As Long
Dim Ro1 As Long, Co1 As Long, LinEnd As Long, LinMax As Long, OrientCol As Long

Application.Calculation = xlCalculationAutomatic 
' ...damit zB dfix-wert richtig ausgerechnet wird
Application.Calculate
Call CheckDef(False)
okay = True

RoCheck = SearchDBTCommand("check") ': MsgBox RoCheck
If RoCheck = 0 Then Exit Function
Ro1 = Range([A2].Value).Row: Co1 = Range([A2].Value).Column
If IsNumeric([a3]) And [a3] <> "" Then LinMax = [a3] Else LinMax = 65536
For Co = 2 To 256
  Cel = UCase(Trim(ActiveSheet.Cells(RoCheck, Co)))
  If Cel <> "" Then
    If Left(Cel, 1) = "§" Then
      Cel = Right(Cel, Len(Cel) - 1) ':      MsgBox cel
      '----------------------------------
      If IsNumeric(Cel) Then  ' ....max. Länge eines Textfeldes testen....
        maxlen = CLng(Cel)
        LinEnd = Ro1 - 1 + Application.WorksheetFunction.CountA( _ 
          Range([A2].Value, Cells(LinMax, Range([A2].Value).Column)))
        For yy = Ro1 To LinEnd
          icel = ActiveSheet.Cells(yy, Co): oldlen = Len(icel)
          While Left(icel, 1) = "?": icel = Right(icel, Len(icel) - 1): Wend
          If Len(icel) <> oldlen Then ActiveSheet.Cells(yy, Co) = icel
          If Len(icel) > maxlen Then 
            okay = False: ActiveSheet.Cells(yy, Co).Select 
            Cells(yy, Co) = "?" & icel '': Stop
          End If
        Next yy
      End If
      '-------------------------------------
      If InStr(Cel, ",") = 0 And Not IsNumeric(Cel) Then _ 
        Call FindAliDef(Co, Cel, okay)  
      ' ... also der normale Fall, 
      '   die offizielle Bezeichnung eines populären Namens zu finden
      If Left(Cel, 2) = "R," Then
        Cel = Right(Cel, Len(Cel) - 2)
        If Cel = "PIG_BM" Then Call FindPigBM(Co, okay)
        If Cel = "ROH_ID" Then Call FindRohID(Co, okay)
      End If
      If Left(Cel, 2) = "V," Then 'Copy Value,Spalte mit Vorgabewerte füllen
        Cel = Right(Cel, Len(Cel) - 2)
        LinEnd = Ro1 - 1 + Application.WorksheetFunction.CountA( _ 
          Range([A2].Value, Cells(LinMax, Range([A2].Value).Column)))
        ''Cells(Ro1, Co1).Select: Selection.End(xlDown).Select
				'' LinEnd = Selection.Row
        Range(Cells(Ro1, Co), Cells(LinEnd, Co)).Value = Cel
      End If
      If Left(Cel, 5) = "DFIX," Then ' ....copy festes datum
        par1 = Mid(Cel, 6, Len(Cel) - 6 - 5) ' feldnamen auslesen zur orient.
        par2 = Right(Cel, 5)
        Range(Cells(Ro1, Co1), Cells(LinMax, Co1)).ClearContents
        OrientCol = WorksheetFunction.Match(par1, [6:6], 0)
        LinEnd = Ro1 - 1 +         Application.WorksheetFunction.CountA( _ 
          Range(Cells(Ro1, OrientCol), Cells(LinMax, OrientCol)))
        ''Cells(Ro1, OrientCol).Select: Selection.End(xlDown).Select
				''LinEnd = Selection.Row
        Range(Cells(Ro1, Co), Cells(LinEnd, Co)).Value = CLng(par2)
      End If
    End If
  End If
Next Co
CheckInput = okay
Range([A2].Value).Select
End Function



Doch halt !
Bevor Sie nun die Flucht ergreifen, lassen sie sich gesagt sein, dass Sie mächtige Verbündete haben, um den Überblick zu behalten:

Lassen Sie sich also nicht entmutigen und verfolgen Sie noch einige Augenblicke diesen Kursus, um einmal zu sehen, was man mit VBA "so alles anstellen" kann! Sie können Datenbanken abfragen und Werte berechnen, in aufwendigen Reports ausgeben und auch zurückschreiben. Sie können andere Windows-Prozesse abfragen und auch anstoßen. Ihrer schöpferischen Ader, ihrem Erfindungsgeist sind fast keine Grenzen gesetzt, sofern Sie die nötige Muße finden! Wenn Sie wirklich wollen, werden Sie sich die Zeit nehmen - wenn nicht, werden Sie hier zumindest einmal einen ersten Einblick gewinnen können, wie weit VBA nützlich sein kann, um typische Büro-Aufgaben zu lösen.

B2.   ↑    Variablen - Namenskonventionen


Variablen sind durch uns benannte Speicherplätze für Zahlen oder Byte-Ketten, die Buchstaben repräsentieren sollen; Buchstaben sind ja laut ASCII-Vereinbarung durchnumeriert: A=65, B=66, C=67..; mit einem Byte (eine Kombination aus 8 ja/nein-Informationen) kann man also 2 hoch 8 = 256 Buchstaben und Sonderzeichen darstellen.

Variablennamen müssen mit einem Buchstaben beginnen, können ziemlich lang sein (mehr als 1 Zeile, was aber natürlich keinen Sinn ergeben würde) und dürfen traditionsgemäß nur aus Buchstaben A bis Z (klein oder groß), den Ziffern 0 - 9 und dem Unterstrich-Zeichen bestehen. Inzwischen sind in VBA auch deutsche Umlaute (Ä, Ö, Ü) erlaubt und einige Sonderzeichen wie °, §, aber noch immer nicht Bindestrich, Leerzeichen und viele andere.

Ich möchte Ihnen hiermit dringend anraten, weiterhin die alten Namenskonventionen einzuhalten - allein, um nicht in Ländereinstellungsprobleme, HTML-Fehlkonvertierungen etc. zu geraten, wenn Sie einmal im Internet Erfahrungen austauschen wollen oder wenn Sie einmal vor einer nicht-deutsch-sprachigen Tastatur sitzen.

Es gibt verschiedene Methoden, seine Variablen systematisch zu benennen - ganze Glaubenskriege toben deswegen. Es gibt Leute, die davon ausgehen, dass man einer Variablen ansehen sollte, welchen Typ sie repräsentiert (Text oder Zahl, Ganzzahl oder gebrochen, 1-Byte, 2-Byte, 4-Byte oder mehr). ZB so: strVorname = String-Typ für Text, lngBuchungen = Long-Typ für 4-Byte-Ganzzahl.

Ich selbst gehöre nicht zu den Anhängern dieser Methode. Denn ich gehe davon aus, dass zB Buchungen nur eine ganze Anzahl sein könnten - oder gibt es etwa 3,7 Buchungen? Weiterhin gehe ich davon aus, dass allein durch die weitere Benennung "Buchungen" anzunehmen ist, dass es sich hier um etwas Zählbares und nicht etwa um den Inhalt von Buchungen im Wortlaut handelt - also Zahl statt Text - wozu brauche ich dann den kleinen Typ-Kürzel "lng" (für lange Ganzzahl) in meiner Variablenbezeichnung ?

Des weiteren wird vielfach gefordert sprechende Variablenbezeichnungen zu wählen. Damit bin ich durchaus einverstanden, solange dabei nicht solche Wortungetüme wie AnzahlPersonenProPassagierflugzeugMaximal herauskommen; man kann eben nicht alles ganz genau in einer Variablenbezeichnung erklären, wenn man gleichzeitig Berechnungsausdrücke, die sich aus mehreren geklammerten Variablenoperationen zusammensetzen, halbwegs lesbar halten will:
[sorte1].Offset(PNum - 1, 2) = [sorte1].Offset(PNum - 1, 2) + pquant
In sprechender Variablenbezeichnungen übersetzt käme etwa so etwas dabei heraus:
[ErsteSorteInProduktliste].Offset(Produktnummer - 1, 2) = [ErsteSorteInProduktliste].Offset(Produktnummer - 1, 2) + Tagesproduktionsmenge
Und dabei handelt es sich noch um ein in seiner Komplexität harmloses Beispiel!

Der Kompromiss zwischen sprechend und lesbar liegt wohl in der Abkürzung und in der weiteren Erklärung der Bezeichnungen als Kommentar neben der Deklarationsanweisung (Dim). Mit Abkürzung meine ich allerdings nicht so etwas kryptisches wie: (x5 - v1) * g7 , sondern schon ein paar Tastenanschläge mehr, die eine ungefähre Vorstellung assoziieren lassen.



B3.   ↑    Speedtest


Wozu braucht man eigentlich Variablen, wenn man doch Werte komfortabel und übersichtlich in Excel-Sheet-Zellen eintragen kann? Excel-Zellen bleiben ja über die Dauer des Programmablaufs hinaus erhalten, während Variablen-Werte nur für die Dauer des Programmablaufs bestehen bleiben - wo liegt also der Vorteil von Variablen?

Zugriffe auf Zellen kosten ein Vielfaches an Prozessor-Zeit und Platzverbrauch als der Umgang mit 'inneren' Speicherstellen für Werte, als die man Variablen bezeichnen könnte. Dies macht sich allerdings erst ab vielen zehntausend Zugriffen bemerkbar, was wir mit diesen beiden kleinen Subs einmal vergleichen können, die Sie in ähnlicher Form in cellspeedtest.xls vorfinden werden.

cellspeedtest



Sub TestCellSpeed() 
[A1] = 0
Do
  [a1] = [a1] + 1
  if [a1] >= 32000 Then Exit Do 
Loop
MsgBox "Fertig!"
End Sub

Sub TestVarSpeed()
Dim a
Do
  a = a + 1
  if a >= 32000 Then Exit Do 
Loop
[B1]=a
MsgBox "Fertig!"
End Sub

Wir sehen also einen Unterschied von dem ungefähr Zehntausendfachen !

Variablen sind außerdem einfacher zu benennen als Zellen: "a=1" ist kürzer als "Range("A1")=1", sogar kürzer noch als die von mir favorisierte kürzeste Form "[A1]=1". Bei zusammengesetzen Ausdrücken aus mehrfachverschachtelten Elementen würde das noch mehr den Programmierfluss bremsen.



B4.   ↑    warum Option Explicit ? (Variablendeklaration erzwingen)

Ich möchte Ihnen hier einmal demonstrieren, was passiert, wenn wir es uns bequem machen und uns selber keine explizite Variablendeklaration abverlangen:

Sub TestNoOptionExplicit()
Do
  Anzahl = Anzahl + 1
  if Anzal >= 32000 Then Exit Do 
Loop

End Sub
Man braucht nicht all zuviel Phantasie aufzubringen, um sich auszrechnen, dass diese Schleife nie - auf natürliche Weise - beendet werden wird, wenn das falsch geschriebene "Anzal" abgefragt wird. - Hätten wir aber den Parameter Option Explicit gesetzt, hätte uns die Fehlerprüfung des VBA-Interpreters darauf aufmerksam gemacht, dass er "Anzal" nicht kennt.

B5.   ↑    Variablentypen


Welche Variablen gibt es?

Viele Sorten von zahlenmäßigen und textmäßigen Ausdrücke werden bereitgestellt, von denen wir uns aber im Rahmen diese Kurses auf folgende beschränken werden:
Long, Double, String, Boolean, Variant.

Long ist ein 4-byte-breiter Ganzzahlenwert zw. rund -2 Mrd und + 2 Mrd; man könnte auch 1-bytige verwerten (Byte), für die die Prozessor-Zugriffszeit seitdem 32-bit-Prozessoren eingesetzt werden gleich ist. Warum unterscheiden wir Ganzzahlen von gebrochenen Zahlen (Dezimal) ? Weil es a) für den Prozessor viel schneller geht, mit ganzen Zahlen zu rechnen und weil b) eine exakte Gleichheit feststellbar ist. Wenn man ein paar Mal hin und her rechnet, wie zB. 100/11/2*2*11, kann dann ohne weiteres 99,9999999989 'rauskommen statt 100; wenn man dann aber den Fall untersuchen will, wo ein Wert =100 gefragt ist, greift das Programm daneben, wenn man die Frage dann nicht umständlich mit Bandbreite formuliert: if a>99,9999 and a<10,00001 then ...

Double ist für Dezimahlzahlen doppelter Rechengenauigkeit (8 Bytes) vorgesehen und die gebräuchlichste Fließkomma-Einheit; einfache Genauigkeit (4 Byte) wäre bei z.B. zweistelligen Millionen-Rechnungsbeträgen, die bis auf den Cent genau angegeben werden müssen, schon zu ungenau. (1 Byte = 8 bit; 1 bit = kleinste Informationseinheit Ja/Nein). Die Prozessor-Zeit-Mehrkosten sind "nur" doppelt so hoch und nicht ein Vielfaches wie bei dem Unterschied zwischen Ganzzahl und Dezimalzahl.

String ist für Texte fast beliebiger Länge vorgesehen (1 einzelnes Zeichen bis mehrere Millionen Zeichen an einem Stück; auch Zahlen können als Texte angesehen werden: Nr. 5 lebt).

Nur der Platz, der wirklich verbraucht wird und ein paar Bytes für die Verwaltung des ganzen werden belegt, wenn man Strings einrichtet und belegt. Ab einer gewissen Größenordnung lohnt es sich, sich darüber gedanken zu machen (mehrere Tausend), was in diesem Kurs aber nicht der Fall sein wird. Wenn es sich irgendwie einrichten lässt ohne all zu kryptisch zu werden, sollten Sie aber im Zweifelsfall immer lieber Zahlen statt Strings einsetzen, denn der Vergleich, ob ein String einen bestimmten Wert hat oder ob eine Ganzzahl einen bestimmten Wert hat, ist natürlich viel Prozessor-aufwendiger mit Strings zu realisieren.

Boolean=Boolsche = richtig/falsch = True/False-Variablen sind im Grunde genommen unnötig, aber äußerst bequem nach ungefähren Sprechgewohnheiten einsetzbar. Man könnte zwar statt dessen ein Byte einsetzen und schreiben: "if verheiratet=1 then..." oder "if verheiratet<>1 then..." , aber wenn man eine boolsche Variable 'verheiratet' definiert und dann schreibt "if verheiratet then ..." oder "if not verheiratet then .." ist das doch viel näher am natürlichen Sprachgebrauch 'dran', oder?

Muss man Variablen deklarieren (anmelden)?

Man muss nicht, aber es wird einem dringend angeraten, um selber die Übersicht zu behalten über die Komponenten, die in den Programmabschnitten eine Rolle spielen.

Schreibt man "Option Explicit" zu Beginn eines Moduls, bedeutet dies, das man sich dazu verpflichtet alle Variabeln, die man verwenden will, explizit (ausdrücklich) anzumelden - und dazu rate ich Ihnen hiermit auch explizit, siehe Option Explicit

Wie deklariert (dimensioniert) man Variablen?

Man schreibt zu Beginn des entsprechenden Programmabschnitts zB folgendes:
Dim a as Long, Runden as Long, Zinssatz as Double, Anrede as String

Variant

Achtung, wenn man zB mehre Long deklarieren will, genügt es nicht, zB so etwas hier zu schreiben: Dim a, b, c as Long - denn nur "c" wäre hier in diesem Fall als Long deklariert, die anderen ("a" und "b") werden zwar 'angemeldet', nicht aber näher spezifiziert. Man weiß also nicht, ob man damit einen Long, einen String oder sonst etwas meint! Ja, auch das ist möglich: unbestimmter Typ, "Variant" genannt, oder eben einfach ohne weitere Bezeichnung.

Wozu ist ein Variant gut?

Zunächst scheint er ja äußerst bequem, aber die Bequemlichkeit wird teuer erkauft: Man wird viel häufiger von der Laufzeitumgebung der Entwicklungsplattform gewarnt, wenn man etwas unsinniges miteinander in Bezug setzt, weil man sich einfach nur im Namen geirrt hat: if Anrede + 3 > 100 ergibt ja wohl keinen Sinn, wenn man mit "Anrede" tatsächlich so ein Wort wie "Herr" oder "Frau Königin" meint; wenn die Variable aber einfach nur "a" hieße statt "Anrede", könnte man den Fehler nicht so schnell erkennen - wohl aber der VBA-Interpreter: Er lässt einen gar nicht erst starten, sondern springt dann vorwurfsvoll und diensteifrig zugleich zur Fehlerstelle, damit man diese korrigieren soll. Variant statt bestimmter Variablentyp lässt oft auch keine Auto-Vervollständigung zu, wenn man seinen Sourcetext schreibt.

Nur wenn ich mich auf sehr unwirtlichem Terrain befinde und mich irgendwie durch den Dschungel der wenig bekannten Objekte durchschlagen muss, erlaube ich mir einmal, die Typ-Frage offenzuhalten, zB hier:

For Each errLoop In Cn.Errors
If errLoop.Number = -2147217843 Then....
ohne mich vorher eingehend danach zu erkundigen von welchem Typ Cn.Errors eigentlich ist....

Es gibt noch das Array = Variablenfeld, zB
Dim Kunden(100,5) As String
was also einem 2-dimensionalem Feld von Strings entspricht; das heißt, dass für 100 Kunden jeweils 5 Strings bereitgehalten werden (zB für Name, Vorname, Titel, Str, Ort). Aber hierauf und auf anderes (Char, Redim,...) will ich im Rahmen unseres kleinen Kurs allerdings nicht näher eingehen.

B6a.   ↑    Public für das ganze Modul


Man sollte sich nur noch folgendes merken: Public statt Dim als Deklarationsschlüsselwort bedeutet, dass die Variable nicht nur in einer Sub oder Function gilt, sondern innerhalb des ganzen Moduls; aber nicht etwa darüberhinaus.

B6b. Über das Modul hinaus
Innerhalb des ganzen Workbooks mit allen Modulen und Forms und sonstigen Teilen muss man dann den betreffenden Modulnamen mit hinzunehmen, um Variablen eines anderen als des eigenen Moduls anzusprechen. Z.B. soll in einer Sub des Moduls "Neben" auf eine Public Variable xy des Moduls "Haupt" zugegriffen werden, spricht man diese von "Neben" aus so an: Haupt.xy - und umgekehrt.
B7.   ↑    Bedingungen


Ohne große Vorrede schauen wir uns einige Formen des Einsatz von If..Then an: ("Case" als Serie von mehren Bedingungen soll hier nicht behandelt werden)


If .. [And (.. Or) ..] Then
..
..
Else
..
..
End If


Ein einfaches Beispiel:
If a > 31 then 
  MsgBox "Auf nächsten Monat ausweichen!"
End If


mehrere Folgen:
If a > 31 then 
  MsgBox "Auf nächsten Monat ausweichen!"
  A =123
  Fertig = True
End If


alles in 1 Zeile (ohne End If):
If a > 31 Then MsgBox "Auf nächsten Monat ausweichen!"


mehrere Folgen in 1 Zeile:
If a > 31 then  MsgBox "Auf nächsten Monat ausweichen!": A=123: Exit Sub


mehrere Bedingungen/Alternativen und Else:
If a > 31 Or b = 0 then 
  MsgBox "Auf nächsten Monat ausweichen!"
  Exit Sub
Else
  MsgBox "Alles Paletti!"	
End If


Achtung ! Bei mehreren Bedingungen werden alle geprüft, auch wenn die erste schon nicht zutrifft!
Dies ist m.E. eine sehr problematische Entscheidung der Entwickler von VBA, denn man kann nicht etwa Typ-Fehler abfangen, indem man so etwas hier bastelt:
a = Range("C8")
If isnumeric(a) and a > 31 Then ....
Wenn in Zelle "C8" also ein Wort (z.B. "Himmel") statt einer Zahl (zB 20) stehen sollte, wird der ganze Ausdruck vom VBA-Interpreter als fehlerhaft bezeichnet und nicht weiter abgearbeitet! Ob der Inhalt von "a" numerisch ist oder nicht - es wird trotzdem weitergeprüft, ob "a", also ob "Himmel" größer 31 ist!

Man muss das Ganze also umständlich umgehen, indem man zB so etwas verschachteltes codiert:
If isnumeric(a) Then
  If a > 31 Then 
    ..
  End If
End If		


Wie schlimm wird die Verschachtelung erst einmal sein bei mehrgliedrigen Bedingungen mit wiederum mehrgliedrigen Unterbedingungen!
B8.   ↑    Schleifen


Ich habe ein Beispiel mit verschiedenen Schleifen bereitgestellt, das Sie aber auch selber eingeben oder durch Kopieren der Textboxen nachbauen können:
Loop.xls
Wir schauen uns folgende Wiederholfunktionen näher an:


Schleifen sind letzten Endes nichts weiter als verschiedene Formen von If .. Then .. Goto - Anweisungen, die bequemer zu bauen und leichter zu lesen sein sollen. Man könnte sicherlich lange darüber debattieren, warum für welchen Zweck die eine besser als die andere einsetzbar ist. In der gleichen Zeit könnte man auch schon weiter programmieren...

Egal für was Sie sich entscheiden - Sie sollten sich auf jeden Fall einen klaren Aufbau angewöhnen, den Sie zusätzlich mit Kommentarzeilen versehen, um Ihr eigenes Konstrukt für Sie selbst und erst recht für andere durchschaubar zu halten. Dies erspart dann lange Grübeleien, wenn man den Code nach Monaten oder Jahren noch einmal verbessern muss.

B8a.    ↑    Do..Loop (~ wiederhole a bis auf weiteres)
Fangen wir mit der einfachsten Schleife an. Sie wird vor allem dann eingesetzt, wenn man nicht eine feste Anzahl Runden vorher festlegen kann, weil dies abhängig sein soll von Faktoren wie Eingaben oder Zwischenergebnissen:
Sub test_loop()
Dim i As Long, b As Long
i = 0: [c8] = 3
Do
  If i = [c8] Then Exit Do
  i = i + 1
  MsgBox i
Loop
End Sub
Wer auch nur die elementarsten Regeln einer mathematischen Gleichung kennt, dem wird die Zeile i = i + 1 sauer aufstoßen! Wie kann eine Variable gleichzeitig ihren Wert und ihren Wert plus 1 haben - das geht doch gar nicht!

Man darf dies eben nicht als Gleichung ansehen, sondern als Zuweisung, auch wenn ein Zeichen = verwendet wird, das wir normalerweise als Gleichheitszeichen ansehen! In Pascal / Delphi zB - aber auch in den meisten der moderneren Programmiersprachen - wird auch zwischen einem Zuweisungsvorgang und einer Gleichheitsabfrage vom Operator her unterschieden; in VBA wird der Einfachheit halber auf solche Spitzfindigkeiten verzichtet.

Mit Zuweisung meine ich ungefähr folgendes:
der Ausdruck links vom Gleichhtiszeichen soll den Wert des Ausdrucks rechts vom Gleichheitszeichen annehmen (hier also: i soll jetzt den alten Wert von i , aber zusätzlich um 1 erhöht, annehmen).




Do .. Loop kann erweitet werden zu:
B8b.    ↑    Do .. Loop Until ..(~wiederhole a bis b ist)
Sub test_while_loop()
Dim i As Long
i = 0: [c8] = 3
Do
  i = i + 1
  MsgBox i
Loop Until i >= [C8] Or i = 10 
End Sub




Do .. Loop kann auch erweitert werden zu:
B8c.    ↑    Do While .. Loop (~wiederhole a solange b ist)
Sub test_loop_until()
Dim i As Long
i = 0: [c8] = 3
Do While i < [c8]
  i = i + 1
  MsgBox i
Loop
End Sub




Statt mit Do While..Loop kann man das gleiche auch sagen mit:
B8d.    ↑    While..Wend (~wiederhole a solange b ist)
Sub test_while_wend()
Dim i As Long
i = 0: [c8]=3
While i < [c8]
  i = i + 1
  MsgBox i
Wend
End Sub




Bei einer festen Anzahl bietet sich ein Klassiker an:
B8e.    ↑    For..Next (wiederhole a genau x mal)
Die Zählervariable ist vom Typ Ganzzahl, weil es nur ganze Runden gibt und nicht etwa 3,7 Runden.
Sub test_for_next()
Dim x As Long
For x = 1 To 3
  MsgBox x
Next x	
End Sub
Auch bei einer For-Next-Schleife, kann man ähnlich agieren wie bei der einfachen Do-Loop-Schleife, indem man zwar so tut, als wenn man es genau wüsste, wieviele Runden stattfinden sollen, aber man eben auch schon vorher abbrechen wird, wenn sich b ergibt; man legt also eine Art von Obergrenze für die Anzahl von Durchläufen an ('wiederhole a genau x mal, aber wenn b eintritt, brich trotzdem schon vorher ab') - Ob das allerdings sauberem Programmierstil entspricht, steht auf einem anderen Blatt:
Sub test_for_next_exit()
Dim x As Long
[c8] =2
For x = 1 To 3
  MsgBox x
  If x >= [c8] Then Exit For
Next x	
End Sub
Im folgenden Beispiel stellen wir einen Vergleichswert von "7" ein, so dass unsere Schleife also nicht vor dem Ablauf der 3 Runden abbricht:
Sub test_for_next_exit()
Dim x As Long
[c8] =7
For x = 1 To 3
  MsgBox x
  If x >= [c8] Then Exit For
Next x	
MsgBox x, , "Wert von x ,nachdem die Schleife beendet worden ist"
If x = 4 Then MsgBox "Zelle kann also nicht 1, nicht 2 und auch nicht 3 sein!" 
End Sub
Wenn Sie einmal genau hinschauen, werden Sie feststellen, dass unsere Zählervariable x um 1 höher ist, als in der Begrenzungsanweisung 1 To 3 steht! Der innere Ablauf des For-Next-Konstrukts läuft offenbar so ab, dass die Zählvariable bei der Next-Anweisung auf jeden Fall um 1 erhöht wird und dann auf den Schleifenbeginn zurückgesprungen wird - auch wenn der Grenzwert (hier 3) schon erreicht ist. Erst beim Schleifenbeginn, wird verglichen, ob die Rundenzahl schon erreicht worden ist: wenn ja wird auf die nächste Anweisung unter dem Schleifenblock gesprungen.

Man hätte durchaus auch andere Spielregeln vereinbaren können. In allen anderen Programmiersprachen, in denen es For-Next-Schleifen gibt (also in fast allen) wird aber aus Traditionsgründen genau so verfahren. Wenn man daran etwas ändern würde, müssten all die Programme neu geschrieben werden, die aus dem Überschreiten des Grenzwerts (hier also bei 4) bestimmte Rückschlüsse ziehen, zB in unserem Beispiel, dass der Wert von [c8] nicht 1, nicht 2 und auch nicht 3 sein kann.


For .. Next kann auch rückwärts zählen:
Sub test_StepBack()
For x = 3 To 1 Step -1
  MsgBox x
Next x	
End Sub


Mit Step kann auch eine andere Schrittweite als minus 1 angegeben werden: Zb plus 3, oder minus 7:
Sub test_Step()
For x = 1 To 6 Step 2
  MsgBox x
Next x	
End Sub




Eine ganz patente Sache ist folgende Konstruktion, die es uns sehr bequem macht, alle Objekte einer Aufzählung durchzunehmen:
B8f.    ↑    For Each ..In .. Next (~Mach etwas für jedes Objekt aus einer Gruppe)
Sub test_foreach()
Dim x
For Each x In ActiveWorkbook.Names
  Debug.Print x.Name, x.Value ' ggf. [Strg+G]=Direct-Fenter einschalten
Next ' oder auch: Next x; aber dies ist nicht zwingend notwendig
End Sub
B9.   ↑    Sub & Function


Alles, was in VB oder VBA geschrieben wird, wird entweder in einer Sub geschrieben (damit ist wohl eine Subroutine gemeint, also eine Art Untersektion) oder in einer Function (womit eine Funktion gemeint ist, ähnlich denen der Mathematik, bei der es immer einen Rückgabewert gibt).
B9a.   ↑    Sub


Eine Sub hat folgenden Aufbau:

Sub abc('ggf. für die übergabe parameter-variablen mit typ deklarieren)
'Dim a, b ... ggf. Variablendeklarationen, automatisch und zwingend privat ..
..
End Sub
z.B. so etwas hier :
Sub ClearPizzaPlan()
If MsgBox("wirklich ALLES löschen?", vbYesNoCancel, "myprog") = vbYes Then
  Range("d4:k35").ClearContents
End If
End Sub
B9b.   ↑    Function


Eine Function hat - wie gesagt - immer genau einen Rückgabewert. Der Aufbau sieht so aus:

Function abc('ggf. für die übergabe parameter-variablen mit typ deklarieren) As Typ (z.B Long) 'Dim a, b ... ggf. Variablendeklarationen, automatisch und zwingend privat
..
..
abc=123 ' <--- Rückgabewert!
End Function


Eine sehr nützliche Funktion ist zB die Osterformel nach Gauss [das ist der Mann mit der komischen Mütze auf den früheren 10-DM-Scheinen. Wenn man bedenkt, dass er dies vor rund 200 Jahren noch mit Tintenfeder und Papier ausgetüftelt hat, hat er diesen Platz in der ehrwürdigen Reihe unserer früheren Banknoten allemal verdient !]

Übergibt man eine Jahreszahl als Parameter, bekommt man eine Tagesnummer als Antwort auf die Frage, auf welches Datum Ostersonntag von einem bestimmten Jahr fällt (Datumsangaben werden in allen Programmiersprachen intern als Tagnummer seit einem bestimmten Zeitpunkt, zB in VBA seit dem 31.12.1899 angesehen).
CGauss


Public Function Gauss_Ostern(A As Long) As Long
Dim D As Long
D = (((255 - 11 * (A Mod 19)) - 21) Mod 30) + 21
Gauss_Ostern = DateSerial(A, 3, 1) + D + (D > 48) + 6 - ((A + A \ 4 + _
D + (D > 48) + 1) Mod 7)
End Function

Sub test_Gauss_Ostern()
MsgBox Format(Gauss_Ostern(2003), "dd.mm.yyyy")
End Sub



Man beachte also, dass eine Function immer einen Rückgabe-Typ im Titel hat und irgendwo (meist am Ende) einen Wert, der rückzugeben ist und hierzu mit der merkwürdigen Form Functionsname = ... zugewiesen wird.

B9c.   ↑    Private


Als Private Sub legt die Excel-VBA-IDE alle neu-angelegten Ereignisse an, zB:
Private Sub CommandButton1_Click()
Damit wird bestimmt, dass diese Sub nur von dem betreffenden Modul aus (meistens ein Sheet-Modul wie "Tabelle1") aufgerufen werden darf. Möchte man sie dennoch von einem anderen Modul aus ansprechen, sollte man das Schlüselwort Private manuell entfernen.

B9d.   ↑    Liste meiner Subs und Functions


Subs und Functions eines Moduls sind bequem anwählbar, so wie hier zu sehen ist:

listofsubs

Lassen wir uns nun zu einem nicht ganz einfachen Kapitel schreiten:

B10.   ↑    Übergabeparameter ByRef oder ByVal


VB und VBA gehen von folgender Regelung aus:

Alle Werte, die in Form von Variablen - statt blanker Eingaben wie Zahlen und Buchstaben - einer Sub oder Function übergeben werden, verändern den Wert dieser Variablen dauerhaft, also so, dass beim Rücksprung in die aufrufende Sub für diese Variablen der nun veränderte Wert gilt, auch wenn die aufgerufene Funktion ganz andere Variablennamen zur Bezeichnung dieser Übergangswerte hat.

Um eine solche Änderung zu vermeiden, muss die aufgerufene Sub oder Function die Übergabeparameter als ByVal deklariert haben - statt ByRef; was aber nicht explizit angegeben werden muss, weil dies die Default (Vorgabe)-Einstellung ist.

Ist dies klar genug ausgedrückt? Bevor Sie jetzt schon aufgeben, versuchen Sie durch langsames Lesen dieser meiner Zeilen den Sinn zu erfassen... ganz ruhig... ;-#


Es folgt nun ein Beispiel aus 3 kleinen Routinen; eine aufrufende (start, unten stehend) und 2 aufgerufene (test2 und test3). Ich kann nun nicht seitenlang beschreiben, was Sie durch schrittweises Verfolgen der Werte in den Messageboxen erkennen können, wenn Sie sich einmal die Mühe machen, mit Copy & Paste den Sourcecode in ein VBA-Modul zu übertragen und dort von Sub Start() aus zu starten. Bitte auch die Titelzeilen der Boxen zu beachten ....

Sub test3(ByVal i As Long, ByVal s As String)
'.. ByVal statt  dem default-mäßigen ByRef!
i = i + 1: s = s + "/wertzuiop"
MsgBox "in test3: " & i, , s
End Sub

Sub test2(i As Long, s As String)
'... ohne Angabe = ByRef statt ByVal
i = i + 1: s = s + "/wertzuiop"
MsgBox "in test2: " & i, , s
End Sub

Sub Start() ' <------- aufrufende Routine
Dim ii As Long, ss As String

ii = 123: ss = "abc"

Call test2(7, "Hallo")
MsgBox "in test1: " & ii, , ss
' test2 verändert nichts,
' weil ja nur blanke werte statt vars übergeben wurden

Call test2(ii, ss)
MsgBox "in test1: " & ii, , ss
' test2 verändert ii und ss, weil vars übergeben wurden
' und defaultmäßig ByRef empfangen wurden

ii = 123: ss = "abc" 'Werte wieder auf alten Wert zurücksetzen

Call test3(ii, ss)
MsgBox "in test1: " & ii, , ss
' test3 verändert ii und ss NICHT,
' denn test3 empfängt übergabewerte explizit als ByVal statt ByRef!

End Sub



Diese Regelung birgt Risiken aber auch Chancen:

Risiken, weil man sich schnell ganz ahnungslos seine Variablenwerte 'verbiegen' lassen kann, wenn man nicht daran denkt, was ByRef bedeutet.

Wenn man aber zB mehrere Werte als Antwort erhalten möchte, braucht man nur die Dinge so laufen lassen, wie die VBA-Erfinder dies eingerichtet haben - also einfach den Default-Wert ByRef (unsichtbar) stehen lassen und schon werden die Werte dauerhaft durch die aufgerufene Sub verändert!

Eine andere Methode bestünde zB darin, auf öffentliche Variablen zurückgreifen, die in einer Sub oder in einer Function geändert werden - was nicht gerade für "gute Programmier-Stube" spricht, denn die Variablen sind möglichst eingekapselt zu halten, um den Code mit all seinen Parametern, Abhängigkeiten und Außenbeziehungen besser überblicken zu können.

- Puh!!! Das war jetzt nicht gerade einfach niederzuschreiben und bestimmt auch nicht einfach zu lesen! ;-o

B11a.    ↑    Inputbox - User-Eingabe


Betrachten wir einfach dieses Beispiel hier:
Sub test_Input()
Dim A 
A = InputBox("Wie alt bist Du?", "Blöde Frage")
MsgBox "Du hast also schon " & Fix(A * 365.25) & " Tage gelebt."
End Sub
Man kann mit InputBox auch Texte und andere Typen abfragen. Achtung! Wenn der User im vorliegenden Beispiel nun ein Wort statt einer Ganzzahl eingibt, kommt es zu Fehlermeldungen! Man kann vor dem Weiterverarbeiten der Eingabe prüfen, ob es sich hier um einen gültigen Zahlenausdruck handelt und sonst den Ablauf abbrechen:
If Not Isnumeric(a) Then Exit Sub
Sub test_input()
Dim A
A = InputBox("Wie alt bist Du?", "Blöde Frage")
If Not IsNumeric(A) Then
  MsgBox "Falsche Angabe, ich arbeite nicht mehr weiter!"
  Exit Sub
End If
MsgBox "Du hast also schon " & Fix(A * 365.25) & " Tage gelebt."
End Sub
B11b.    ↑    If MsgBox - User-Entscheidung


Enscheidungsfragen an den User kann man so formulieren:
Option Explicit

Sub test()
If MsgBox("Willst Du wirklich dieses tolle Programm beenden, ohne deine Daten abzuspeichern?", _
  vbYesNo, "Eine ernste Frage an Dich") = vbYes Then
  MsgBox "Na gut, du hast es so gewollt!": Exit Sub
Else
  MsgBox "Wir machen also weiter..."
End If
End Sub
B12.   ↑    Diverse VBA-Innereien
(Goto / Stop / Exit Sub / End / Call / _ / : )


Hier folgen noch einige Kleinigkeiten aus dem Inneren von VBA, also lauter Dinge, die man auch in Visual Basic (VB) an sich vorfinden würde - ganz unabhängig von der Application Excel.

B12a.    ↑    Goto
Goto ist als Sprunganweisung in Fachkreisen ziemlich verpönt. Ich denke aber, dass man sich keinen Abbruch tut, wenn man hin und wieder einmal Goto verwendet, um sehr komplizierte Verschachtelungen zu vermeiden oder um sich unsinnige Aufteilung von kleinsten Schritten in eigene Module zu ersparen.

Goto abc, wobei die Sprungmarke abc: am linken Zeilenrand definiert wird:

Sub Test()
Dim a, b, c
a = [C8]: b=[D8]
If a = b Then Goto hierhin
'..
'..
hierhin:
MsgBox "Mama, fertig! "
End Sub
B12b.    ↑    Stop
Entspricht der Aufforderung, den Programmablauf genau dort zum Debuggen anzuhalten.

Ist also im Gegensatz zum Haltepunkt nicht nur vor dem Zeilenbeginn, sondern an einer beliebigen Stelle des Quelltexts einsetzbar. Man kann dann auch zB nur unter bestimmten Bedingungen den Programmablauf zum Debuggen unterbrechen, um sich einen bestimmten Fall näher anzuschauen:
Sub test()
Dim a As Long
For a = 1 To 5
  If a = 3 Then Stop
  MsgBox a * a
Next a
End Sub
B12c.    ↑    Exit Sub (Exit Function)
Entspricht der Aufforderung, die aktuelle Subroutine (Function) dort zu verlassen, also ohne nachfolgende Anweisungen derselben Routine abzuarbeiten.

Wenn diese Anweisung in einer Unterroutine steht, die von einer Hauptroutine aus aufgerufen wird, werden die nachfolgenden Anweisungen der Hauptroutine fortgesetzt (im Gegensatz zu der End-Anweisung).
Sub Verteil_Aepfel(Aepfel As Long, Personen As Long)
If Personen = 0 Then Exit Sub
MsgBox "Jeder erhält also " & Aepfel / Personen & " Äpfel.", , Personen
End Sub

Sub test_Verteil_Aepfel() ' die aufrufende Routine, mit F5 starten
Dim i As Long
For i = 3 To 0 Step -1
  Call Verteil_Aepfel(8, i)
Next i
MsgBox "So, alles erledigt"
End Sub
B12d.    ↑    End
Entspricht der Aufforderung, den Programmablauf genau dort zu beenden und keine weiteren Operationen in anderen Programmsegmenten auszuführen.

Kann man zB dafür einsetzen, um abzusichern, dass Operationen nur unter vernünftigen Bedingungen ausgeführt werden (zB Monatsplanung nur bis zum letzten Tag des Monats erlauben). Diese Bedingungen sollte man schon im Vorfeld abklären, so weit man mögliche Komplikationen voraussehen kann, so dass man in diesen Fällen die entsprechenden Subs erst gar nicht aufruft, sondern schon die aufrufende Routine selber (zB eine Click-Ereignisbehandlung für einen Button) beendet per Exit Sub.

Manchmal jedoch ist es nicht möglich, alles schon in der aufrufenden Routine abzuklären, weil bestimmte Parameter unter bestimmten Bedingungen erst in den Subroutinen ermittelt werden können/sollen. Dann aber soll nicht nur diese Subroutine beendet werden (per Exit Sub), sondern der ganze Programmablauf, also auch alles, was sonst noch in der aufrufenden Routine danach geschehen sollte.

Man könnte aber ein End auch in einem solchen Fall vermeiden, in dem man jeder subroutine eine Boolsche Variable mitgibt, in der der Erfolg der Operation zurückgegeben wird. Den nächsten Schritt in der Hauptroutine lässt man dann nur dann ausführen, wenn diese Variable = True ist ...

Das nachfolgende Beispiel wäre in diesem Sinne also ganz klar auch ohne die End-Anweisung zu regeln:
Sub Verteil_Aepfel_End(Aepfel As Long, Personen As Long)
If Personen = 0 Then
  MsgBox "Alarm! " & vbCr & "Habe alles abgebrochen, weil sonst durch Null geteilt worden wäre!!!"
  End
End If
MsgBox "Jeder erhält also " & Aepfel / Personen & " Äpfel.", , Personen
End Sub

Sub test_Verteil_Aepfel_End() ' die aufrufende Routine, mit F5 starten
Dim i As Long
For i = 3 To 0 Step -1
  Call Verteil_Aepfel_End(8, i)
Next i
MsgBox "So, alles erledigt" 'wird hier nie ausgeführt werden wegen End
End Sub
B12e.    ↑    Call
Dieses Schlüsselwort wird immer dann verwendet, wenn man die Parameter einer Prozedur in Klammern angeben möchte, zB: Call MsgBox("Hallo","Test-Titel")

statt direkt: MsgBox "Hallo",,"Test-Titel".

Das Schlüsselwort Call hat darüber hinaus den Vorteil, dass man auf den ersten Blick erkennen kann, dass es sich bei dem nachfolgenden Ausdruck um eine Sub oder Function handelt, im Gegensatz zu anderen Ausdrücken wie Befehlswörter zum Schleifenbau (For Next), Variablennamen (x), Objekte (Range("A1").Offset(2, 3)) oder Objekteigenschaften (Range("A1").Value).
B12f.    ↑    _ (Unterstrich um die Zeile bis zur nachfolgenden zu verlängern)
Dieses Zeichen wird immer dann verwendet, wenn man die Zeile umbrechen will, um sie lesbarer zu machen, aber der Interpreter das Geschriebene weiterhin als eine einzige Zeile auffassen soll, zB so hier:
Sub test()
If MsgBox("Willst Du wirklich dieses tolle Programm beenden, ohne deine Daten abzuspeichern?", _
  vbYesNo, "Eine ernste Frage an Dich") = vbYes Then
    MsgBox "Na gut, du hast es so gewollt!": Exit Sub
End If
End Sub
B12g.    ↑    : (Doppelpunkt um in selber Zeile eine weitere Anweisung einzuleiten)
Hin und wieder erlaube ich mir, mehrere Anweisungen in einer einzigen Zeile zu schreiben, in dem ich einen Doppelpunkt (:) als Trennzeichen verwende:
Sub test(i As Long)
If i = 0 Then MsgBox "Ich darf nicht durch Null teilen!" :  Exit Sub
MsgBox "100 geteilt durch " & i & " = " & 100 / i
End Sub

Sub test_test() ' mit F5 hier starten
Call test(4)
Call test(0)
End Sub
Diese Vorgehensweise wird bei Puristen als schlechter Stil angesehen. Ich hingegen denke, dass seitenlange Quelltexte, in denen nicht viel mehr als ziemlich leere Zeilen mit End If als einziger Anweisung erscheinen, nicht gerade klarer zu handhaben sind, wenn man bedenkt, dass man oft die nächste Bildschirmseite "aufschlagen" muss, um ein komplexes Konstrukt in seiner vollen Länge zu erfassen.

Dort also, wo nicht viel mehr folgt als zB eine Meldung an den User mit anschließendem Exit Sub, weil gewisse Parameter nicht erfüllt wurden, kann man m.E. durchaus auch einmal alles in eine einzige Zeile unterbringen, in dem man Gebrauch vom Doppelpunkt als Trennzeichen macht. Dies sollte aber letzten Endes jede(r) für sich selbst entscheiden ...
 
X1a.   ↑    zum Beispiel: Pizza-Fabrik Produktionsplan


Es ist nun an der Zeit, unsere frisch erworbenen Kennnisse über den Umgang mit der Entwicklungsumgebung, über Zellzugriffe und Schleifen an einem größeren Beispiel zu erproben:
Für eine Pizza-Fabrik mit 3 Produktionsstraßen soll ein Monatsplan erstellt werden. Für jede Straße wird Pizza-Typ und Tagesmenge abzüglich Umstellverluste eingetragen.
excel_pizzaplan7


In dieser ersten Übungsserie hier gehen wir vom manuellem Eintragen der gewünschten Sorten und Mengen aus und summieren die Produktionsmengen nach Sorten gruppiert auf, indem wir eine 2-fach-geschachtelte Schleife bauen für alle 3 Öfen und alle Tagesmengenzellen eines Monats. Danach folgen einige Vorbereitungen zum Ermitteln des Produktvorgängers, um den Wert für einen entsprechenden Produktwechsel aus der Change-Tabelle zu entnehmen.

Im Teil C - Button & Form - werden wir das Bauen von Formularen mit visuellen Objekten kennenlernen. Dabei arbeiten wir praktischerweise am Beispiel der Pizza-Produktionsplanung weiter, um Auswahllisten für die Pizza-Sorte und Eingabefeldern für Mengenzielvorgaben anzubringen.

Danach folgt eine zweite Übungsserie, in der wir die Formularelemente nutzen, um eine Ablaufsteuerung für die Planungsvorgänge einzubauen.

Abschließend bauen wir einige Schleifen, die von diesen Objekten gesteuert werden, um Produktionsabschnitte einzufügen mitsamt der Serie an Tagesmengen, die sich aus der jeweiligen Umstellungskombination ergeben. Dabei werden wir auch das Verlängern einer Kampagne ermöglichen, in dem wir die nachfolgenden Produktionsbschnitte nach unten veschieben.

Beim folgenden Ausdruck und Extrakt des bloßen Plans als separate Datei lernen wir den Makro-Rekorder und seine Grenzen kennen (Teil D).

Wir werden nun nicht jede einzelne der ca. 500 Zeilen nachbilden und ausführlich besprechen - dies können Sie in Ruhe selbst studieren, in dem Sie das fertige Beispiel untersuchen; die wichtigsten Funktionen werden wir aber gemeinsam aufbauen.

Fertig ist das Planungsprogramm ohnehin noch lange nicht: Eingabeprüfung, Bereichsgrenzensicherung, Datenablage für verschiedene Monate, usw. fehlen; die Navigation wäre per Kontextmenü-Steuerung zu verbessern; dem User müssten Kontextsensitive Hilfetexte anboten werden, und ... und ... und ....



X1b.    ↑    Dateien zum Pizza-Demo in Entwicklungsstufen
Ich habe eine normale Excel-Datei ganz ohne VBA-Programmierung vorbereitet, die man sich am besten 'runterlädt oder in ähnlicher Form aufbaut, um daran eigene Programmierungsschritte vorzunehmen:
pizza01_purecells.xls


Die Datei besteht zunächst einmal lediglich aus 2 Excel-Sheets:

sheet "Plan" ...

excel_pizzaplan



... und sheet "Change":

excel_pizzachange



Nicht unwichtig dabei sind die vorbereiteten Namen für Zellbereiche. Nehmen Sie diese bitte erst einmal hin, ohne all zu lange über diese nachzudenken; wir werden für alle später noch den Einsatzweck kennenlernen:

gotorange



(Mit der F5-Taste lässt sich ja bekanntlich die Liste der Bereichsnamen anzeigen)

Folgende Bereichsnamen sollten auf den beiden Sheets enthalten sein:

pizzaranges Links wird der Bereichsname und rechts daneben die jeweilige Adresse angegeben.

Beachten Sie, dass nicht nur einzelne Zellen, sondern auch Zellverbände ("Sortiment" und "sums") hier verwendet werden.

Als einziger Bereichsname für das sheet "Change" ist "Nullpunkt" in Zelle "C3" definiert.

(Wie man Bereichsnamen definiert, wird hier erklärt.)



Und so soll das Ganze dann am Ende aussehen, wenn wir unsere Programmierung angebracht haben:

excel_pizzaplanfinished



X1c.    ↑    Funktionen ("Features", Ausstattungsmerkmale):


X1d.     ↑    Warum gerade dieses Beispiel?
Vielleicht habe ich ein Beispiel konstruiert, das nicht direkt Ihrer persönlichen Aufgabenstellung entspricht; ich habe aber bewusst einen Fall gewählt, in dem man nur wenige Daten als Ausgangsmaterial benötigt, um dann darauf um so mehr Programm-Logik ansetzen zu können.

Viele der sonst üblichen Beispiele (Vertreterprovisionen berechnen) zeigen Dinge, die man besser ganz einfach mit Excel-Zellformeln erledigt. Man sollte nie die Möglichkeiten des großen Zellfunktionsangebots unterschätzen! Es sind oft auch Beispiele, für die echte Datenbank-Lösungen angebracht wären (Videofilme archivieren). Das ist nämlich der Fall, sobald eine bestimmte Größenordnung erreicht wird; oder wenn mehrere User darauf verändernd zugreifen, ohne dass die Datensicherheit dabei zu kurz kommen darf.
X2.    ↑    function findProdNum - Welche Produktnr. hat dieser Pizzaname


Bevor wir die Mengen der Pizza-Kampagnen nach Pizzasorten getrennt aufsummieren können, müssen wir eine Funktion schreiben, die herausfindet, in der wievielten Zeile von oben eine bestimmte Sorte eingetragen ist.

Diese Aufgabe könnte man auch direkt in der Summier-Sub einbauen; da wir dieselbe Teilfunktion aber auch nützen können zum Ermitteln der Produktintensität je nach Pizzatyp laut Produktliste in Spalte O bis P des Haupt-sheets und zur Ermittlung des Umstellzeitverlusts in der Change-Tabelle, machen wir eben eine separate Funktion daraus.

Wir wollen von Anfang an alles das, was nicht einem bestimmten Button zuzuordnen ist, auf einem separaten Modul "main" eintragen, das wir per rechter Maustaste im Project-Fenster des VBA-Screens erstellen: einfügen Modul; Eigenschaften-Fenster (name)=main statt Modul1 als Vorgabewert.

In main geben wir auch später einige wenige Variablen und Konstanten an, die wir von verschiedenen anderen Modulen aus brauchen werden.

Konstanten sind einfach Bezeichner ähnlich wie Variablen, die eine bestimmte Zahl oder ein bestimmtes Wort ersetzen, aber ohne dass diese sich noch einmal ändern sollen. Bekannteste Konstante in der Mathematik ist Pi (3,14...).

Wir tragen jetzt schon einmal in der ersten freien Zeile des main-Moduls nach Option Explicit die Konstante maxProds an, in der wir die max. Anzahl an verschiedenen Pizzasorten eintragen:

Public Const maxProds = 50


Ich versuche nun, die Vorgehensweise der zu schreibenden Funktion erst einmal in natürlicher Sprache auszudrücken, in dem ich sage, was ich mache, wenn ich eine bestimmte Sorte suche:

Geh deine Pizzaliste vom 1.Feld an solange zeilenweise zählend herunter, bis du das gewünschte Wort findest; gib die Suche auf, wenn Du über dem letzten Eintrag der Liste hinweg bist. Gib die Zeilennummer als Antwort an den Aufrufer zurück.

Der Kopf der Funktion ist daher schon klar:
Function findProdNum(ByVal was As String) As Long
und die eigentliche Schleife könnte dann so aussehen, wenn Y die Zeilennr. ab der "sorte1" sein soll und maxProds schon vorher definiert wurde:
While Range("sorte1").Offset(Y, 0) <> was And Y < maxProds
  Y = Y + 1
Wend
Wir erinnern uns, dass While..Wend soviel bedeutet wie: 'Solange a .. ist, mach weiter ..', oder so.

Für den Fall, dass nichts gefunden wurde, soll "0" zurückgegeben werden, sonst eben den Abstand von Startpunkt aus plus 1:
If Y >= maxProds Then Y = 0 Else Y = Y + 1 findProdNum = Y
Die ganze Function sieht dann so aus:
Function findProdNum(ByVal was As String) As Long
Dim Y As Long, c
If was = "" Then findProdNum = 0: Exit Function
Y = 0: c = ""
While Range("sorte1").Offset(Y, 0) <> was And Y < maxProds 
  Y = Y + 1 
Wend 
''MsgBox y
If Y >= maxProds Then Y = 0 Else Y = Y + 1
findProdNum = Y
End Function


Und so können wir die Function testen:
Sub test_findProdNum()
MsgBox findProdNum("Milano")
End Sub


Wir positionieren uns mit dem Cursor in die Test-Routine und drücken die F5-Taste und erfahren per MsgBox, dass "Milano" die 4.Sorte unserer Liste ist - womit nun unsere erste Funktion geschrieben und für gut befunden worden ist!
X3.    ↑    Calc - berechne die Mengen nach Pizzatyp getrennt


Und jetzt werden wir die Function findProdNum einsetzen können, um die Pizza-Sorten mengenmäßig zu erfassen.

Wir zerlegen das Problem, in dem wir erst einmal versuchen, die Vorgehensweise unseres Programms in natürlichen Worten darzustellen:

"Durchlauf die Ofen1-Monats-Liste in Spalte D vom ersten bis letzten Tag des Monats und stelle fest, welches Produkt gerade hergestellt wird. Hierbei gilt, dass solange kein anderer Produktname erscheint, der zuletzt gelesene weiter gültig ist.

Nimm aus der Spalte rechts daneben die Tagesmenge und trage diese in der Produktenliste von Spalte O-Q in Spalte Q ein, und zwar in die Zeile, in der laut Function findProdNum der Pizzatyp zu finden ist. Zähle hierbei zu dem, was du dort als Menge vorfindest die Tagesmenge hinzu und lege sie dort wieder ab.

Wenn du mit der ersten Ofenstraße fertig bist, mache das gleiche noch 2 mal je 3 Spalten weiter nach rechts beginnend für Ofen2 und Ofen3."

Puh, ganz schön viele Worte für einen eigentlich ganz simpel anmutenden Sachverhalt!

Wir legen einen Button cmdCalc an (wie das geht, müssten Sie eigentlich schon wisen, sonst schauen Sie hier noch einmal nach: A4 und A7) , machen bei aktivem Entwurfmodus einen Doppelklick auf den Button und erzeugen somit eine Sub auf dem Modul zum Sheet Tabelle1 (Plan):
Private Sub cmdCalc_Click()
..
End Sub
Hier werden wir also unseren weiteren Code unterbringen.

Ich fange mit einer einzigen Ofenstraße (Ofen1) erst einmal an und lege eine Schleife an, die vom 1. bis letzten Tag herunterläuft. Dazu soll vorher in der Const maxTage=31 zu Beginn des main-Moduls festgelegt werden, dass es maximal 31 Tage in einem Monat (und max. 50 verschiedene Pizzatypen) geben kann/soll:
Public Const maxTage = 31, maxProds = 50
Jetzt aber zurück zu unserer Sub cmdCalc_Click() auf dem Plan-sheet-Modul. Mit Dim müssen wir dort erst noch Variablen anlegen: y als Zeilenzähler und c für den Eintrag in der Produktspalte und pquant für die Menge - und auf geht's:

Private Sub cmdCalc_Click()
Dim y as Long, c, pquant
  For Y = 1 To main.maxTage
    c = Range("line1").Offset(Y - 1, 0)
    pquant = Range("line1").Offset(Y - 1, (Line - 1) * 3 + 1)
  Next Y
End Sub

Die Sub teste ich ohne Button-Click, in dem ich mich mit dem Cursor irgendwo in den Quelltext stelle und auf die F5-Taste drücke.

Und wenn man nun vor der "Next Y"-Zeile einen Haltepunkt setzt, in dem man einfach mit der linken Maustaste auf den grauen Rand klickt (noch einmal klicken zum wieder-entfernen), kann man Zeile für Zeile verfolgen, welche Werte in y, in c und in pquant gesetzt sind. Hierzu fahren wir vorsichtig mit dem Mauszeiger über die gewüschte Variable und lassen diesen hier stehen, während wir mit der F5-Taste weiterschalten zur nächsten Runde...

stepbystep

Nach der Zuweisung von c (c = ..) schieben wir eine Bedingung ein, mit der wir ptype immer dann den Wert von c annehmen lassen, wenn c nicht leer ist. Bei leerer Zelle, bleibt also der alte Name in ptype erhalten.

Zusätzlich ermitteln wir, welche Produktnummer (die wievielte Zeile in unsere Produktliste) für ptype zu finden ist. Dazu benutzen wir zum ersten Mal unsere vorher geschriebene Function findProdNum, der wir in Klammern gesetzt den ProduktNamen mitgeben, für den wir die Listennummer haben wollen - das Ergebnis soll in die Variable pnum geschrieben werden:
    If c <> "" Then
      ''aha, neue Kampagne....
      ptype = c
      pnum = findProdNum(ptype)
    End If
So steht also Produktname immer fortgesetzt in ptype (vorausgesetzt, dass bei Monatsbeginn auf jeden Fall ein Produktname eingetragen wird).

Nach der pquant-Zuweisungszeile (pquant = ..) fügen wir folgende Zeile ein, die den Tagewsmengenwert in der 2.Spalte rechts neben der Produktliste akkumuliert:
    [sorte1].Offset(pnum - 1, 2) = [sorte1].Offset(pnum - 1, 2) + pquant
Was geschieht hier genau? Zugriff auf die Mengenspalte Q der Produktliste in P:Q des sheets "Plan" erfolgt so: Von der Bereichszelle "sorte1" (die steht in plan!O5) aus per offset pnum minus 1 Schritte heruntergehen und 2 Spalten nach rechts. Dieser Ausdruck soll den Wert von diesem Ausdruck, aber erweitert um die Tagesmenge pquant annehmen - fertig!

Um den Ablauf diesmal ohne Haltepunkte direkt auf dem Sheet verfolgen zu können - denn dort sehen wir ja die Ofenstraße und die Mengenspalte der Produktenliste fügen wir noch folgende Zeile nach der Akkumulierung ([sorte1].offset....) hinzu:
    MsgBox pquant, , ptype
Jetzt probieren wir das Ganze hier einmal aus ...
Private Sub cmdCalc1_Click()
Dim y As Long, c, pquant, ptype, pnum As Long
  For y = 1 To main.maxTage
    c = Range("line1").Offset(y - 1, 0)
    If c <> "" Then
      ''aha, neue Kampagne....
      ptype = c
      pnum = findProdNum(ptype)
    End If
    pquant = Range("line1").Offset(y - 1, 1)
    [sorte1].Offset(pnum - 1, 2) = [sorte1].Offset(pnum - 1, 2) + pquant
    MsgBox pquant, , ptype
  Next y
End Sub
Prima! Es läuft ja soweit schon ganz gut, aber es stört noch, dass die alten Summen nicht gelöscht werden und so von Run zu Run immer weiter anwachsen!

So helfen wir dem ab, in dem wir folgende Zeile an den Anfang setzen:
Range("sums").ClearContents
wobei "sums" die Mengenspalte von Q5:Q20 ist - wurde vorher durch ganz normale Excel-Bereichsdefinition angelegt.

Nun bleibt nur noch eines übrig:

Nicht nur 1, sondern alle 3 Ofenstraßen sollen summiert werden. Wir bauen also um die y-Schleife eine Line-Schleife (Line 1-3 für unsere Ofenstraßen). Damit aber nicht immer die 1.Straße 3 mal aufsummiert wird, müsen wir den Zugriff auf die Straßen-Spalten flexibilisieren:

Anstatt im Offset-Ausdruck Spalten=0 zu setzen, werden wir nun für den Zugriff auf die Produktzelle (Line-1)*3 anweisen; das gleiche für den Zugriff auf die Mengenspalten: statt 1 setzen wir (Line-1)*3+1 ein.

Und hier ist nun die fertige Sub:
Private Sub cmdCalc_Click()
Dim Y As Long, Line As Long, PNum As Long
Dim c, ptype, pquant
Range("sums").ClearContents
For Line = 1 To 3
  For Y = 1 To main.maxTage
    c = Range("line1").Offset(Y - 1, (Line - 1) * 3)
    If c <> "" Then
      ''aha, neue Kampagne....
      ptype = c
      ''MsgBox y, , "neue Kampagne"
      PNum = findProdNum(ptype)
    End If
    pquant = Range("line1").Offset(Y - 1, (Line - 1) * 3 + 1)
    ''MsgBox ptype, , pquant
    [sorte1].Offset(PNum - 1, 2) = [sorte1].Offset(PNum - 1, 2) + pquant
  Next Y
Next Line
End Sub


So, jetzt können wir noch einmal testen und ... voilà!

pizzaplansums



Zum Vergleich von unserer VBA-Programmierung mit Mitteln der Excel-Zellfunktionen habe ich in der Spalte R Formeln zum bedingten Summieren angelegt (SummeWenn, für jede Spalte eine). Diese funktionieren aber nur dann, wenn es keine Leer-Einträge bei der Produktbezeichnung gibt, wie es aber in unserem Ofenplan um der besseren Lesbarkeit willen der Fall ist. Wir müssten sonst die Produktbezeichnungen Tag für Tag wiederholen und würden so einen Produktwechsel nicht auf dem ersten Blick erkennen könnnen, wenn es sich um 2 Produkte handelt, die sehr ähnlich geschrieben werden (zB Mista1 und Mista2).

Wir sehen also, dass wir mit VBA alles und mehr machen können, was mit Zellformeln möglich ist! Und wenn Sie bis jetzt noch nicht überzeugt waren von den Möglichkeiten, die uns Excel-VBA bietet, sollten Sie es spätestens jetzt sein!

Ich jedenfalls bin so begeistert, dass ich mir erst einmal die Beine vertreten muss! Diese Beispiele machen ja irgendwie hungrig...

Sie könnten inzwischen für den Fall, dass Sie nicht ganz mitgekommen sind oder etwa keine Lust hatten alles selbst aufzubauen, das eben erarbeitete Beispiel hier nachladen:
pizza02_calcsum.xls
X4a.    ↑    Function findVorProd - Finde das Vorgänger-Produkt


Wir werden jetzt noch einige weitere Funktionen für Arbeitsschritte schreiben, die immer wieder in späteren komplexen Operationen wie das Einfügen oder Löschen einer Produktionskampagne gebraucht werden. Sie sollten hier zumindest die erste der folgenden Funktionen selber aufbauen, um das Prinzip zu verstehen; die darauf folgenden müssen Sie nicht unbedingt Zeile für Zeile nachvollziehen.

Den Produktvorgänger brauchen wir für mehrere Operationen, unter anderm auch, um aus der Kombination von Vorgänger und neu-einzufügendem Produkt einen Umstellverlust aus der entsprechenden Tabelle herauszusuchen.

Wir gehen ähnlich vor wie in der Function findProdNum, und zwar ungefähr so:
Geh mit dem Zellzeiger so lange einen Schritt nach oben, bis du auf einen anderen Produktnamen stößt (oder über die erste Zeile hinaus geraten bist).
Wir brauchen also unserer Funktion keinen Parameter für Zeile und Spalte zu übergeben, da wir voraussetzen, dass ab dem Zellzeiger mit der Suche begonnen werden soll. Als Antwort wollen wir den Produktnamen erhalten. (Wir hätten uns natürlich auch die Zeilennummer übergeben lassen können.) Daraus folgt nun folgender Funktionskopf:
Function findVorProd() As String
Die aktuelle Position des Zellzeigers übergeben wir an vorher definierte Variablen:
Dim x As Long, Y As Long
x = ActiveCell.Column: Y = ActiveCell.Row
wobei wir die Zelle c erst einmal als leer definieren.

Nun bauen wir die eigentlich Schleife:
Do
  Y = Y - 1
Loop Until Cells(Y, x) <> "" Or < 4
Und zwar so, dass wir sofort damit beginnen einen Schritt nach oben zu gehen, und zwar so lange , bis etwas anderes als nichts vorgefunden wird, also etwas vorgefunden wird oder die 4.Zeile unterschritten wurde. In der 4.Zeile könnte das Produkt des letzten Monats stehen.

Wir könnten natürlich auch andere Schleifen wählen, um das gleiche zu erreichen:
Do While Cells(Y, x) <> "" And Y >= 4
  Y = Y + 1
Loop
- Aber es könnte ja gut möglich sein, dass wir vor unserer Suche auf einer Zelle stehen, auf der ein Produktname eingetragen ist; dann würden wir also sozusagen nur uns selbst finden; in diesem Fall müssten wir also zuerst noch einen Schritt nach oben machen und dann erst unsere Schleife beginnen. Diesen Schritt nach oben dürften wir aber nur dann machen, wenn wir nicht ohnehin schon in der 4.Zeile stehen.... - Wir sehen also, dass einiges für unsere Do .. Loop Until-Schleife spricht.

Jetzt müsen wir nur noch die Funktionsrückgabe definieren und das Funktionsende schreiben; dabei machen wir aber folgende Fallunterscheidung: Wenn wir ein Vorgängerprodukt gefunden haben, geben wir es als Funktionsergebnis wieder; ansonsten geben wir einen Leerstring ("") zurück:
If Y < 4 Then findVorProd = "" Else findVorProd = Cells(Y, x)
End Function
So, die ganze Funktion sieht dann also so aus:
Function findVorProd() As String
Dim x As Long, Y As Long
x = ActiveCell.Column: Y = ActiveCell.Row
Do
  Y = Y - 1
Loop Until Cells(Y, x) <> "" Or Y < 4
If Y < 4 Then findVorProd = "" Else findVorProd = Cells(Y, x)
End Function
Und zum Testen können wir diese kleine Sub hier nehmen:
Sub testfindVorProd()
MsgBox findVorProd
End Sub
(Zum Testen den Zellzeiger bitte in eine Spalte mit Produktwechseln (D oder G oder J) setzen und Funktion von VBA-Quelltext aus mit F5-Taste starten, ggf. Haltepunkte setzen, um den Ablauf zu beobachten.)
X4b.    ↑    Function findNextProdRow - Nachfolgerzeile finden
Weil wir nun einmal so schön dabei sind, schreiben wir noch ein paar ähnliche Funktionen. Wie bereits gesagt, Sie müssen nicht unbedingt Zeile für Zeile mitmachen, wenn Sie glauben, Sie kommen auch schon so ganz gut zurecht ...

Die Zelle [M1] benennen wir "maxro" - wir werden später noch erfahren, wie die letzte Zeile eines Monats errechnet wird, vorerst tragen wir einfach manuell den Wert "35" für einen 31-tägigen Monat ein.

In Excel kann man Bereichsnamen bekanntlich angeben, in dem man einen bestimmten Zellbereich markiert (oder einfach nur für 1 einzige Zelle den Zellzeiger irgendwohin stellt) und dann aus dem Excel-Menü Einfügen / Namen / Festlegen wählt und dort den Namen eingibt. Wenn Ihnen das immer noch zu 'hoch' ist, schauen Sie sich bitte in C3c an, wie man das auch noch machen kann.
setrange


Das nachfolgende Produkt finden wir ganz ähnlich wie in der findVorProd-Funktion - nur, dass wir aus bestimmten Gründen diesmal die Zeilennummer wiedergeben werden statt Namen ( Function findNextProdRow() As Long ) und natürlich vorwärts statt rückwärts gehen ( Y = Y + 1 ) , mit dem unteren Rand [maxro] als Begrenzung und ab einer Zelle unter dem Zellzeiger beginnend ( Y = ActiveCell.Row + 1 ) . Außerdem wählen wir diesmal keine Do.. Loop Until-Konstruktion sondern While .. Wend.

Hier ist schon die ganze Funktion:
Function findNextProdRow() As Long
Dim x As Long, Y As Long
x = ActiveCell.Column: Y = ActiveCell.Row + 1
While Cells(Y, x) = "" And Y <= [maxro]
  Y = Y + 1
Wend
findNextProdRow = Y
End Function
Und hier wieder die Test-Hilfe:
Sub test_findNextProdRow()
MsgBox findNextProdRow
End Sub
Lassen Sie uns noch einmal Zeile für Zeile die Funktion mit eigenen Worten wiedergeben:
Ab 1 Zelle unterhalb des Zellzeigers (ActiveCell.Row + 1) solange weiterrücken (While .. Y = Y + 1 .. Wend ), wie nichts in der Zelle zu finden ist und man sich noch innerhalb des Berichs bis incl. Zeile 35 [maxro] befindet. Dann den Wert der jetzigen Zeilennummer (y) wieder zurückgeben an den Funktionsaufrufer (findNextProdRow = Y). - Fertig!


Na, war das jetzt schwer?

- Ich denke ja, schon! Machen Sie sich nichts daraus, wenn Sie selbst in so kurzen Zeilen wie den obigen etliche Probleme haben, die Sache zum Laufen zu bringen. Jeder, der zu programmieren beginnt, muss solche dornigen Wege gehen. Da hilft nur probieren, leichte Änderungen vornehmen und wieder probieren, bis man langsam ein Gefühl für die vielen Kleinigkeiten gewinnt, die es zu beachten gilt. Lassen Sie sich von mir auch keinen bestimmten Weg diktieren! Fühlen Sie sich frei, andere Konstruktionen anzuwenden, wenn Sie eine Idee haben !
X4c.    ↑    Function findProdBeginRow - Beginn eines Produktionsabschnitts finden
Diese Aufgabe stellt sich dann, wenn wir einen ganzen Produktionsabschnitt markieren wollen, aber mit dem Zellzeiger nicht genau auf der ersten Zeile eines Produktionsabschnitts stehen. Wenn Ihnen nicht klar ist, wozu das gut sein soll, warten Sie ab, bis wir diese Funktion in einer Lösch-Prozedur einsetzen werden.

Hier ist schon die ganze Funktion:
Function findProdBeginRow() As Long
Dim x As Long, Y As Long
x = ActiveCell.Column: Y = ActiveCell.Row
While Cells(Y, x) = "" And Y > 5
  Y = Y - 1
Wend
findProdBeginRow = Y
End Function
Hier der Tester:
Sub test_findProdBeginRow()
MsgBox findProdBeginRow
End Sub
Und hier wieder die Erklärung in Form einer Arbeitsanweisung an den Computer in natürlicher Sprache:
Solange du keine nicht-leere Zelle vorfindest und die Zeile des ersten Monatstags (5) nicht unterschreitest, schau eine Zelle darüber nach.
So ähnlich könnte man das formulieren. Steht man bereits bei Funktionsaufruf auf dem Produktnamen, wird eben diese Zeile als Ergebnis übergeben, ohne dass die Schleife durchlaufen wird.

Den Zellzeiger bewege ich dabei übrigens keinen einzigen Schritt - ich lese ja nur den Inhalt der Zellen aus; würde man den Zellzeiger mitwandern lassen (ActiveCell.Row = ActiveCell.Row + 1), würde man bei längeren Suchen (zB bis zur 65536.Zeile) ein Vielfaches an Geschwindigkeitsverlust erleiden!
X4d.    ↑    Produktionsabschnitt löschen
So, jetzt endlich können wir unsere schönen Funktionen für etwas Nützliches einsetzen: Wir werden den Produktionsabschnitt, in dem der Zellzeiger gerade steht von der ersten bis zur letzten Zeile erkennen und diesen Bereich zum Löschen markieren.

Dabei zerlegen wir die Aufgabe in Teilschritte, die wir wieder in natürlicher Sprache vorformulieren werden:
Ermittle den Produktionsabschnittsbeginn ab Zellzeigerposition und die Zeile über dem nächsten Produktionsabschnitt darunter und markiere diesen Bereich 2 Spalten breit. Stell noch einmal die Frage, ob der User wirklich löschen will; wenn ja, dann lösch!


Wir erstellen einen CommandButton irgendwo oben auf dem Excel-Sheet "Plan" und geben diesem den Namen cmdClear1. Mit Doppelklick (bei aktivem Entwurfmodus) erzeugen wir die noch leere Click-Behandlungsroutine im Tabelle1(Plan)-Modul. Diese befüllen wir nun mit unseren Anweisungen.

Wir legen erst einmal eine Variable Co für die Spalte an, in dem der Zellzeiger zur Zeit steht und eine Variable Ro1 für die Zeile bei Produktionsbeginn und Ro2 für die letzte Zeile des aktuellen Produktionsabschnitts:
Private Sub cmdClear1_Click()
Dim Ro1 As Long, Ro2 As Long, Co As Long
Co = ActiveCell.Column
Ro1 = main.findProdBeginRow
Ro2 = main.findNextProdRow - 1
End Sub
Vor dem Ende fügen wir noch die Markierungsanweisung ein:
Range(Cells(Ro1, Co), Cells(Ro2, Co + 1)).Select
Dabei erinern wir uns an die Möglichkeit, einen Zellbereich (Range) aus 2 Zellen Cells(,) zusammenzusetzen, wobei die erste Zelle den Bereich links oben und die zweite den Bereich rechts unten meint.

Den markierten Bereich könen wir dann ganz einfach mit Selection.ClearContents löschen.

Wir wollen dem User aber noch eine Chance geben, die Operation abzubrechen, falls er nur versehentlich auf unseren Clear1-Button geklickt hat; dies tun wir mit folgendem If-MsgBox-Konstrukt (wurde in B11b erklärt) :
If MsgBox("wirklich diese Kampagne löschen?", vbYesNoCancel, "myprog") = vbYes Then
  Selection.ClearContents
End If
Danach setzen wir den Zellzeiger noch zurück auf die ursprünglich vorgefunden Position mit:
Cells(Ro1, Co).Select

Und hier ist nun die ganze Prozedur:
Private Sub cmdClear1_Click()
Dim Ro1 As Long, Ro2 As Long, Co As Long
Co = ActiveCell.Column
Ro1 = main.findProdBeginRow
Ro2 = main.findNextProdRow - 1
Range(Cells(Ro1, Co), Cells(Ro2, Co + 1)).Select
If MsgBox("wirklich diese Kampagne löschen?", vbYesNoCancel, "myprog") = vbYes Then
  Selection.ClearContents
End If
Cells(Ro1, Co).Select
End Sub
Doch halt! Etwas fehlt noch: Wir stellen fest, dass wir zwar Kampagnen löschen wie die Weltmeister, aber immer noch die alten Summen in der Produktliste stehen lassen; diese sollten wir noch einmal auffrischen, in dem wir nach dem Selection.ClearContents noch eine Zeile einfügen: Call cmdCalc_Click.



Ich hoffe, dass bei Ihnen alles geklappt hat und denke, dass wir nach all den theoretischen Schleifen dieses kleine praktische Erfolgserlebnis gebrauchen konnten! Haben Sie bitte noch eine Viertelstunde Geduld für einige Restarbeiten in Sachen Funktionen, bis wir endlich zum Kapitel der visuellen Objekte stoßen werden - die Ungeduldigen können natürlich schon vorher dorthin vorpreschen, wenn sie überhaupt bis hierher ausgeharrt haben ...
X5a.    ↑    Function calcQuantperday - Tagesmenge ermitteln
Wir schreiben eine einzeilige Funktion, die wir nur anlegen, um uns den sperrigen Ausdruck abzukürzen, den wir sonst noch öfter benötigen würden, um auszurechnen, wieviel von einem bestimmten Produkt auf einer bestimmten Ofenstraße täglich produziert wird - wenn man einmal Umstellungsverluste außer Acht lässt.

Wir brauchen als Eingabeparameter die ProduktSpalte (also die Ofenstraße), für die wir die Angabe berechnen wollen - in der 2.Zeile wird ja die Kapazität angegeben; wir bilden folgenden Ausdruck zum Lesezugriff:
Cells(2, Co + 1)

Und wir brauchen dazu die Produktnummer, die wir vorher schon irgendwie ermittelt haben (zB durch Verwendung unsrer allerersten Funktion findProdNum), um aus unserer Produktliste die Angabe zu holen, mit welcher Intensität das betreffende Produkt produziert wird; wobei "100%" bedeuten würde, dass die maximale Straßengeschwindigkeit erreicht wird - einige Pizza-Sorten benötigen eben mehr Produktionszeit als andere. Den Zugriff hierauf machen wir so:
[sorte1].Offset(PNum - 1, 1).
[sorte1] ist der Bereichsname der 1.Pizzasorte in der Liste in der Zelle [O5], in der "Margherita" steht. Diesen Bereichsnamen bitte ich anzulegen, falls Sie ihn nicht schon vorgefunden haben. Die Offset-Methode wurde schon in A13c. Offset erklärt.
Function calcQuantperday(Co As Long, PNum) As Double
'Tagesleistung=Ofenkapazität * produktspezifische Intensität
calcQuantperday = Cells(2, Co + 1) * [sorte1].Offset(PNum - 1, 1)
End Function

Sub test_calcQuantperday()
MsgBox calcQuantperday(4, 5)
End Sub


Und schon haben wir unsere Funktion samt Tester geschrieben; ein Test mit 4 und 5 als Parameter bedeutet also: Rechne Tagesmenge für Ofen1 und ProduktNummer 5 aus (hier 126 = 140 * 90%).
X5b.    ↑    Function calcDaysforchange - Umstellungsdauer ermitteln
Die nächste Funktion ist ebenfalls nur als Ersatz für einen sperrigigen ZugriffsAusdruck auf unsere Umstellungstabelle, den wir uns künftig durch Aufruf eben dieser Funktion erleichtern wollen.

Wir schauen uns noch einmal die Umstellungstabelle an ...

excel_pizzachange



... und stellen uns die Frage, wie diese zu interpretieren ist. Man sucht das neue Produkt in der Zeile 3 und geht dann so weit nach unten, bis man in der Spalte B das Vorgänger-Produkt entdeckt, zB ergibt der Wechsel zu Milano von Napoli kommend 1,8 Tage Ausfallzeit infolge von Produktumstellung.

Für unsrer Funktion gehen wir aber davon aus, dass die Produktnummern schon vorher bekannt sind und der Auftrag also nur lautet:
Geh in der Umstelltabelle vom [Nullpunkt] aus neu-Produktnummer Zellen nach rechts und alt-Produktnummer Zellen nach unten und gib diesen Wert zurück.
Wir legen einen Funktionskopf mit den Eingangsparametern PNum für das neue Produkt und PreNum für das alte an und geben einen Double-Wert zurück; Ganzzahlen wären hier nicht ausreichend, weil in der Tabelle ja Zahle mit 1 Dezimalstelle hinter dem Komma stehen.

Und eigentlich könnten wir mit einer einzigen Zeile zurecht kommen:
Function calcDaysforchange(PNum As Long, PreNum As Long) As Double
calcDaysforchange = [change!Nullpunkt].Offset(PreNum, PNum)
End Function


Da wir aber auch solche Werte berücksichtigen werden, die sich aus einem Wechsel zu sich selber ergeben (also zB von "Tonno" zu "Tonno", auch wenn dies erst einmal wenig sinnvoll erscheinen mag) oder aus einem Wechsel von nichts zu einem neuen Produkt (wenn vorher Stillstand auf der Ofenstraße war), müssen wir diese Fälle noch von den normalen Wechseln unterscheiden und mit 0 Verlust-Tage wieder zurückgeben. Die ganze Function wird dann so erweitert:
Function calcDaysforchange(PNum As Long, PreNum As Long) As Double
'Umstelltage aus Tabelle holen, falls wirklich P-Wechsel vorliegt...
If PNum <> PreNum And PreNum > 0 Then
  calcDaysforchange = [change!Nullpunkt].Offset(PreNum, PNum)
Else
  calcDaysforchange = 0
End If
End Function
Und hier direkt die entsprechende Test-Sub:
Sub test_calcDaysforchange()
MsgBox calcDaysforchange(4, 2)
End Sub
X6.    ↑    Sub WriteQuants - Produktionsabschnittsmengen eintragen


Lassen Sie uns nun zum krönenden Abschluss all unserer Bemühungen im Reich der Funktionen kommen!

Was müssen wir unternehmen, wenn wir folgende Operation durchführen wollen:

Trage von hier bis zur nächsten Produktionskampagne Tag für Tag die Produktionsmengen für ein bestimmtes Produkt ein. Es kann dabei laut Umstellungstabelle bis zu 3 Tage Produktinsausfall zu beginn der Kampagne geben.

Unsere Sub soll von folgendem ausgehen: Aktueller Produktname und -Nummer und die vom vorangehenden Produkt sollen ermittelt werden. Dabei gehen ja unsere find-functions immer vom derzeit gesetzten Zellzeiger aus; daher werden wir diesen auch jedesmal vorher setzen mit
Cells(Ro, Co).Select
Dies führt zunächst einmal zu folgendem Aufbau:
Sub writeQuants(Ro As Long, Co As Long)
Dim P As String, Pre As String, PNum As Long, PreNum As Long

'Produktnummern von Produkt und Vorgänger ermitteln...
Cells(Ro, Co).Select
P = Cells(Ro, Co): PNum = findProdNum(P)

Cells(Ro, Co).Select
Pre = findVorProd: PreNum = findProdNum(Pre)

End Sub
Die test-sub hierzu schreiben wir schon jetzt, damit wir in allen Bauphasen prüfen und debuggen können:
Sub test_writeQuants()
Call writeQuants(12, 7)
End Sub
Wir testen dies erst einmal. Dafür stellen wir sicher, dass in der Zelle [G12] unseres "plan"-sheets ein Produktname steht, hier zB Roma, und als Vorgänger möglichst auch noch eines aufgeführt wird (Vulcano). Bevor wir die Test-Sub starten, setzen wir noch vor dem Ende der Hauptroutine (writeQuants) einen Haltepunkt. So können wir uns den Wert der Variablen anzeigen lassen:

debugwritequants

Wenn Sie nicht genau wissen, wie man mit Haltepunkten debuggen kann, lesen Sie bitte den Abschnitt D2. Debug

So, kommen wir zum 2. Bauabschnitt: Wir müssen auch noch zum Nachfolge-Produkt springen, um herauszufinden, wo unsere neue Kampagne endet. Dies tun wir mit folgenden Zeilen:
'wieviel tage lang ...
Cells(Ro, Co).Select
nextRo = findNextProdRow '.. also nachfolger suchen um kampagnenende zu kriegen
Days = nextRo - Ro
Hierzu müssen wir aber die Variablen nextRo und Days noch anlegen:
Dim nextRo As Long, Days As Long


Als letzte Vorübung ermitteln wir Tagesproduktionsmenge und Umstelldauer, indem wir die entsprechenden Funktionen, die wir einige Seiten vorher vorbereitet haben, nutzen; Variablen hierzu und eine Zählvariable D sind vorher anzulegen:
Dim Daysforchange As Double, Quantperday As Double, D As Long
'wieviel produzieren wir pro normalen tag (also ohne umstellverlust) ...
Quantperday = main.calcQuantperday(Co, PNum)

'wieviel umstelltage...
Daysforchange = main.calcDaysforchange(PNum, PreNum)
Zusmmengenommen haben wir also jetzt folgenden Abschnitt:
Sub writeQuants(Ro As Long, Co As Long)
Dim P As String, Pre As String, PNum As Long, PreNum As Long
Dim nextRo As Long, Days As Long
Dim Daysforchange As Double, Quantperday As Double, D As Long

'Produktnummern von Produkt und Vorgänger ermitteln...
Cells(Ro, Co).Select
P = Cells(Ro, Co): PNum = findProdNum(P)

Cells(Ro, Co).Select
Pre = findVorProd: PreNum = findProdNum(Pre)

'wieviel tage lang ...
Cells(Ro, Co).Select
nextRo = findNextProdRow '.. also nachfolger suchen um kampagnenende zu kriegen
Days = nextRo - Ro

'wieviel produzieren wir pro normalen tag (also ohne umstellverlust) ...
Quantperday = main.calcQuantperday(Co, PNum)

'wieviel umstelltage...
Daysforchange = main.calcDaysforchange(PNum, PreNum)

End Sub

Jetzt fehlt nur noch die eigentliche Verteil-Schleife. Da wir wissen, für wieviele Zeilen wir die Tagesmengen eintragen sollen, bietet sich hier eine For-Next-Schleife an, in der wir versuchsweise schon einmal die normalen Tagesbeträge eintragen, als wenn es keine Umstellverluste geben würde:
For D = 0 To Days - 1
  Cells(Ro + D, Co + 1) = Quantperday
Next D
Wir testen ... Okay, jetzt werden also gleichmäßig über alle 8 Tage der Wert 72 eingetragen, was 90% Roma-Produktintensität von der Ofen2-Kapazität 80 ist.

Jetzt kommt der schwierigste Teil: Wir müssen die Verlusttage eintragen. Wir gehen dafür von folgender vereinfachten Überlegung aus: Wir ziehen von den ermittelten Verlusttagen Runde für Runde einen Tag ab, bis wir Null erreicht haben. Solange es noch Verlusstage gibt, tragen wir 0 als Tagesmenge ein und im anderen Fall eben die Tagesmenge (72):
'Tagesmengen eintragen...
For D = 0 To Days - 1
  If Daysforchange > 0 Then
    Cells(Ro + D, Co + 1) = 0
    Daysforchange = Daysforchange - 1 '  .. 1 tag weniger zu berücks.
  Else
    Cells(Ro + D, Co + 1) = Quantperday
  End If
Next D

Testen wir dies wiederum, stellen wir fest, dass wir zwar schon einen guten Schritt weiter gekommen sind, da ja die ersten 3 Tage mit "0" und die darauffolgenden mit "72" eingetragen worden sind.

Aber es fehlt noch eine Kleinigkeit: Der 3. Tag ist nur ein angebrochener Verlusttag - laut Tabelle ist der Wechsel von "Vulcano" zu "Roma" 2,4 Tage teuer; dh. für den 3. Tag müssen wir den entsprechenden übriggebliebenen Teil errechnen statt einfach "0" einzutragen:
(1 - Daysforchange) * Quantperday
Wenn also DaysforChange > 0 und < 1 ist, soll diese Teil-Menge eingetragen werden.

Wir müssen also die positive Folge unserer If-Weiche ( If Daysforchange > 0 Then .. ) noch einmal unterteilen für den Fall a) wo der ganze Tag mit "0" berechnet wird und für den Fall b) wo der angebrochen Rest eingetragen wird:
If Daysforchange >= 1 Then
  Cells(Ro + D, Co + 1) = 0
  Daysforchange = Daysforchange - 1 '  .. 1 tag weniger zu berücks.
Else
  Cells(Ro + D, Co + 1) = (1 - Daysforchange) * Quantperday
  Daysforchange = 0 ' nichts mehr zu berücks.; ab nächstem tag volle leistung
End If
Beachen Sie auch das Reduzieren der Umstelltage-Menge: wenn es mindestens noch einen ganzen Tag gibt, wird einfach 1 Tag abgezogen; gibt es nur noch einen Tagesrest, wird dieser ganz auf "0" gesetzt - und fertig.

So, das war jetzt nicht gerade einfach, denke ich mir mal. Aber glauben Sie mir bitte: Wenn Sie das Geschäft hier eine ganze Weile betrieben haben, werden Sie für einen solchen Lösungsansatz weniger Zeit (ca. 20 Minuten) brauchen als jetzt, wo Sie das Ganze lesen und in Stufen nachbauen und testen müssen!

Hier noch die komplette Prozedur:
Sub writeQuants(Ro As Long, Co As Long)
'Tagesmengen eintragen - dabei umstellzeit berücksichtigen
Dim P As String, Pre As String, PNum As Long, PreNum As Long
Dim Days As Long, nextRo As Long
Dim Daysforchange As Double, Quantperday As Double, D As Long

'Produktnummern von Produkt und Vorgänger ermitteln...
Cells(Ro, Co).Select
P = Cells(Ro, Co): PNum = findProdNum(P)

Cells(Ro, Co).Select
Pre = findVorProd: PreNum = findProdNum(Pre)

'wieviel tage lang ...
Cells(Ro, Co).Select
nextRo = findNextProdRow '.. also nachfolger suchen um kampagnenende zu kriegen
Days = nextRo - Ro

'wieviel produzieren wir pro normalen tag (also ohne umstellverlust) ...
Quantperday = main.calcQuantperday(Co, PNum)

'wieviel umstelltage...
Daysforchange = main.calcDaysforchange(PNum, PreNum)

'Tagesmengen eintragen...
For D = 0 To Days - 1
  If Daysforchange > 0 Then
    If Daysforchange >= 1 Then
      Cells(Ro + D, Co + 1) = 0
      Daysforchange = Daysforchange - 1 '  .. 1 tag weniger zu berücks.
    Else
      Cells(Ro + D, Co + 1) = (1 - Daysforchange) * Quantperday
      Daysforchange = 0 ' nichts mehr zu berücks.  - ab nächstem tag volle leistung
    End If
  Else
    Cells(Ro + D, Co + 1) = Quantperday
  End If
Next D
End Sub

Unser Zwischenstand ist als File abgelegt:
pizza03_find.xls


Jetzt fehlt uns nur noch ein knackiges Formular mit Auswahlliste für die Pizza-Sorte und dann können wir sehr schnell Produktionskampagnen einfügen. Schleifen-bauen haben wir ja nun bis zum Abwinken geübt!
 

 ↑  C. Button & Form - Buttons, Listen, Formularbau

C1.    ↑    Events - Ereignisse wie Button_click und Before_Close)


Es gibt mehrere vorgefertigte Ereignisse, die unsere Toolbox-Objekte betreffen, wovon das häufigste der normale Click ist. Es gibt aber noch einige andere, je nach Objekttyp. Beim Eingabefeld ist es zum Beispiel ratsam, auf das Ereignis "Exit" einzugehen: immer wenn jemand das Eingabefeld mit dem Cursor verlässt (zB durch Enter bei Eingabe-Ende), wird dieses Ereignis mit einer bestimmten Berechnung behandelt.

Die mögliche Liste der Ereignisse ist im VBA-Fenster einer Form oben rechts anwählbar, probieren Sie dies einmal aus! Sie werden feststellen, dass Sie durch bloße Auswahl entsprechende Subroutinen als Container für Ihren Code erzeugen...

othereventtypes.png



Im nachfolgenden Beispiel wird das Ereignis "MouseMove" für den 2.Button implementiert: Immer wenn man den 2. Button mit dem Mauszeiger auch nur berührt, springt eine Meldung hervor:

othereventsheet.png

Der Quelltext zu MouseMove (CommandButton2 auf Tabelle1-Sheet-Modul):
Private Sub CommandButton2_MouseMove(ByVal Button As Integer, _
   ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
MsgBox "Halt!"
End Sub




Wenn man im "Diese Arbeitsmappe" für Workbook das Event "Open" wählt, kann man dort angeben, was pasieren soll, wenn das File geöffnet wird:

workbook_open.png



und der Quelltext zu workbook_open (Diese-Arbeitsmappe-Modul):
Private Sub Workbook_Open()
MsgBox "Welcome to that file, my friend!"
End Sub
Das Beispiel kann man auch laden:
othereventtypes.xls
C2a.   ↑    Form anlegen


Wir bleiben weiter an unserem Pizza-Plan-Beispiel, um ein Formular mit einer Auswahlliste und Eingabefeldern und andere sichtbare Objekte zur bequemen Bearbeitung des Produktionsplans beizusteuern. Wir werden hier also das Visual von VBA einstudieren.

Wir sollten zu diesem Zweck den bisher erarbeiteten Stand der Programmierung vor uns aufgeschlagen haben; um sicher zu sein, dass wir alle vom selben Stand ausgehen, sollten wir dieses File hier laden (nicht direkt vom Browser aus öffnen, sondern vorher abspeichern auf lokale Festplatte):
pizza03_find.xls


Wir legen zunächst eine Form an. Eine Form ist ein Fenster, das eingeblendet werden kann für verschiedene Toolbox-Objekte, wie z.B. Listboxen, Eingabefelder, Okay-Buttons etc. Irgendwo im Project-Fenster klicken wir die rechte Maustaste und wählen "Einfügen : UserForm" - und schon erscheint ein graues leeres Fenster mit samt Toolbox.

Wenn diese nicht erscheinen sollte, klicken Sie sich diese im VBA-Pulldown-Menü Ansichten herbei. Wenn die Toolbox im Weg sein sollte, verschieben Sie sie weiter nach rechts oder unten hin.

excel_newform

C2b.   ↑    Form benennen und betiteln


Um die Form später aus unseren VBA-Zeilen heraus ansprechen zu können, geben wir ihr einen leicht wiedererkennbaren Namen, der sich deutlich von dem Vorgabewert unterscheidet: "frmJob". Hierzu verwenden wir das Eigenschaftenfenster unten links, das jetzt automatisch auf die Eigenschaften unserer neuen Form zeigen sollte. Dort verändern wir die Vorgabewerte ("UserForm1") von (Name) auf "frmJob" und von Caption auf "Pizza Product Job".

Das Eingenschaftenfenster funktioniert wie ein Monitor: Wenn wir später z.B. ein anderes Gruppenelement als unsere "frmJob" bearbeiten, zB den VBA-Code zu den Objekten des sheets "Plan", wird das Eigenschaftenfenster wiederum automatisch die entspr. Dinge des anderen Objekts anzeigen.

excel_formsetting

C3a.    ↑    Listbox anlegen und mit Daten in Excel-cells verknüpfen


Nun fügen wir unserer noch leeren Form ein erstes Toolbox-Objekt hinzu, indem wir "Listbox" (Listenfeld) auswählen, genau so, wie es für Button in unserem Hello-World-Beispiel schon erklärt wurde (hier) :

excel_newlist

C3b.     ↑    Rowsource
Wir verbinden die Listbox mit einer Liste von Produktnamen (Prosciutto, Margherita, Roma...), die wir im Excelsheet im Zellbereich O5 bis O20 angelegt haben (O wie Otto, nicht Null!) - per RowSource-Eigenschaft:

excel_listboxdata2



C3c.    ↑    Relative Adressierung per Bereichsname ist besser als die nackte Adresse
Noch besser als die 'nackte' Adresse ist es, einen Bereichsnamen anzugeben, zB. "Sortiment".

Hierzu wechseln wir zum Excel-Arbeitsblatt "Plan" und richten einen solchen Namen für den Zellbereich O4:O20 ein (incl. Überschrift):

setrangename


Bitte den Bereich O4:O20 wählen, auch wenn die Markierung so aussieht, als wenn sie erst in der 5.Zeile begänne! (O wie Otto, nicht Null!)

In Excel kann man Bereichsnamen bekanntlich angeben, in dem man einen bestimmten Zellbereich markiert (oder einfach nur für 1 einzige Zelle den Zellzeiger irgendwohin stellt) und dann oben links im Namenfeld einen BereichsNamen vergibt.

Achtung! Gibt es diesen schon, springt der Zellzeiger dorthin, ohne den gewünschten Bereich zu benennen, was viele User erst gar nicht mitkriegen! Dummerweise muss man in einem solchen Fall mit [Alt]+E / N / F (bzw. D) die alte Benennung löschen, bevor man also denselben Namen auf einen anderen Bereich verschieben kann! - Dies gehört allerdings zum Excel-Grundwissen und sei nur am Rande unseres Excel-Programmierkurs' erwähnt.


Unser Bereichsname ("Sortiment") hat den Vorteil, dass man den Bereich später verschieben kann, ohne dass die Rowsource-Property danebengreift; denn Rowsource würde beim Verschieben nicht etwa automatisch die nackte Adresse anpassen, so wie wir es von einfachen Zellformeln wie Summe(A1:A10) her kennen, wohingegen ein Bereichsname gegen solche Missgriffe gefeit ist. Und verschieben werden wir wahrscheinlich noch eine ganze Menge im Laufe unserer Projektentwicklung...

Das Prinzip der relativen Adressierung per Bereichsname ist auch in vielen anderen Bereichen von Excel und VBA bedeutend und darüberhinaus in der ganzen 'Computerei' !

Wenn der Bereichsname noch nicht angelegt worden ist, akzeptiert die Excel-VBA-IDE eine Bereichsnamenangabe in Rowsource nicht. Ist dann alles richtig eingestellt, sehen wir schon im Form-Design-Mode die gewünschte Liste.
C3d.    ↑    Listbox mitsamt der UserForm starten - per Button
So, jetzt wollen wir endlich unsere Liste ausprobieren - aber wie?

Die Liste ist in der UserForm (frmJob) verankert. Wir müssen also irgendwie die Userform starten. Aber auch hier wissen wir nicht, wie.

Wir legen erst einmal einen Button zum Starten der Form an (Excel-sheet, Toolbox, Befehlsschaltfläche...) und nennen diesen (per Rechtsklick Eingenschaften: name) cmdNewP und beschriften seine Oberfläche (caption) mit "NewP" für neues Produkt einfügen in einen Plan. Per Doppeklick auf den Button (bei noch immer aktivem Entwurfmodus) erzeugen wir eine Sub cmdNewP_Click(), die das Click-Ereignis für diesen Button regelt.

Wir sagen nichts weiter als:
Private Sub cmdNewP_Click()
frmJob.Show
End Sub


C3e.    ↑     AutoCompletion
Dabei ist uns folgendes aufgefallen: Sobald wir den Punkt hinter frmJob eingegeben haben, klappte eine Auswahlliste mit allen Eigenschaften und Aktionen (gelb unterlegt) des Objekts links vom Punkt, also in dem Fall unsere UserForm frmJob. Daraus wählen wir show; wenn wir die erstesten 2 Buchstaben eingegeben haben, springt die Liste auf 'show' und braucht dann von uns nur noch durch antippen der Tab-Taste (links neben der Q-Taste) bestätigt werden. Bei einem so kleinen Wort wie 'show' mag dies keine große Erleichterung sein, wohl aber bei längerern Ausdrücken. Außerdem sehen wir, welche Eignschaften und Methoden (Aktionen) zum gewählten Objekt erhältlich sind - also rundherum eine feine Sache!

autocompletion.png
C3f.    ↑    List-Click auswerten
Haben wir jetzt endlich alles zusammen?

Wir gehen voller Erwartung auf unser Excel-Sheet, stellen den Entwurfmodus ab und klicken enldich auf den [newP]-Button, und...: Ja, die Listbox entfaltet sich in ihrer ganzen Pracht! - aber immer wenn wir eine Sorte auswählen, passiert rein gar nichts! Wir können also die Form vorerst nur per [x]-Symbol am oberen rechten Fenster-Rand schließen.

Damit sich dieses ändert, müssen wir noch das Ereignis List-Click auswerten!

Dazu nehmen wir wieder unsere Userform auf dem VBA-screen vor (Doppelclick auf [frmJob] im Project-Fenster oben links). Die Form des Excel-Sheets wurde hoffentlich schon vorher geschlossen - wenn nicht bitte nachholen. Mit einem Doppelklick inmitten auf die Listbox eröffnen wir die Ereignisbehandlungsroutine (Sub) für Listenauswahl:
Private Sub ListBox1_Click()

End Sub
Und dort hinein schreiben wir erst einmal:
Private Sub ListBox1_Click()
MsgBox ListBox1.ListIndex
End Sub


C3g.    ↑    Test
So, schnell wechseln zum Excel-Sheet, Entwurfmodus aus und ausprobiiiiiieren:

pizzalistbox.png
C3h.    ↑    ggf. Liste noch mal checken
Na, hat's geklappt?

Wenn nicht, folgendes noch einmal checken:
Listbox bauen
C4.    ↑    andere Toolbox-Objekte : Label, Textbox, Options


toolboxlegend Labels - sind für Textausgaben (nicht für Texteingaben) vorgesehen. Es kann sich hier um feste Überschriften o.ä. handeln, aber auch um Werte, die sich während des Programmlaufs ändern und als Label ausgegeben werden.

TextBoxes - Ich finde das dt. "Textfeld" unterscheidet sich nicht eindeutig genug von Label/Bezeichnungsfeld. Jedenfalls ist der Inhalt der Texteingabe in der Eigenschaft Value zu finden. Könnte also auch als Textausgabe-Box missbraucht werden, indem man Value besetzt. Ereignis zur Abfrage des so eben Eingegebenen ist Exit !

OptionButton - Es handelt sich vorgabemäßig um entweder-oder-Schalter; es sollten also mindestens 2 davon angelegt werden, die sich dann wechselseitig automatisch ausschließen, ohne dass man da noch extra programmieren muss. Man kann also für genau einen davon den Value vorgabemäßig = True eintragen.



C4b.   ↑    Objekte in Form anlegen


Wir werden nun neben der Listbox in unserer Form zusätzliche Toolbox-Elemente hinzufügen, bis wir diese Form hier ungefähr aufgebaut haben werden:

pizzaform.png


Wir beginnen mit dem Typ Label (dt. "Bezeichnungsfeld") . Da wir im ersten Feld neben der ListBox1 nur eine Überschrift haben, auf die wir nicht weiter von VBA aus zugreifen werden, lassen wir den Default-Name unverändert (Label4 oder so).

Dann sollen Labels angelegt werden, denen wir folgende Namen von oben nach unten geben werden: "lbDaystomonend", "lbDaystonext", "lbDaystoprolong".

Jetzt legen wir 2 Optionbuttons untereinander an, deren Namen wir unverändert beim Defaultwert lassen (Optionbutton1 und -2). Für den ersten setzen wir die Value-Eigenschaft auf "True" - einfach so in dem Eigenschaftenfenster eintragen.

Neben jeden Optionbutton kommt eine TextBox. Die erste nennen wir "tbDays", die andere "tbQuantperdays".

Jetzt fügen wir noch Cancel-Button (cmdCancel) und OK-Button (cmdOK) ein und haben somit den äußeren Aufbau unserere Form erledigt.

Und hier ist das Excel-file mitsamt Summierung nach Sorten und Listbox für die Pizza-Sorten-Auswahl als Zwischenstandsfile abgelegt:
pizza04_listbox.xls
C5.    ↑    Objekt-Logik - Ablauf steuern durch ausblenden


Bevor man durch Button-Click eine Userform aufruft, kann man einige Dinge vorbereiten: Kurz: Man kann einen erheblichen Teil der Steuerung des Dialogs mit dem User auf die Anlage der visuellen Objekte verlegen. Dabei sind die Objekt-Eigenschaften visible (sichtbar) und enabled (aktiviert) - neben den Ereignisbehandlungsroutinen - die wichtigsen Mittel, um solche Abläufe zu gestalten.

So wollen wir zB dafür sorgen, dass zuerst ein Element aus einer Liste gewählt wird und dann erst Ausgabefelder in der Form eingeblendet werden, in denen Informationen zu dem Gewählten stehen; außerdem sollen dem User dann Eingabefelder zur Verfügung gestellt werden, in denen er zu dem gewählten Element Vorgaben machen kann.

Natürlich könnte man in alter Großrechner-Terminal-Manier alle erdenklichen Informationen und Eingabemöglichkeiten in ihrer prächtigen Vielfalt auf den ganzen Bildschirm ausbreiten und darauf hoffen, dass der User schon weiß, was er einzugeben hat. Besser aber ist es, dem User Schritt für Schritt nur die Eingabemöglichkeiten zu zeigen, die er für die aktuelle Operation braucht und sonst nichts!

Wir würden in so einem Fall also auf ein Listbox_Click hin bei korrekter Wahl ein Eingabefeld Textbox1.Visible = True setzen und bei Abbruch diesen Wert auf False setzen. Was der User nicht sieht, kann er auch nicht einstellen. So ist also beiden gedient: Der User wird nicht verwirrt von Einstellungsmöglichkeiten, sondern er sieht diese erst im Zusammenhang des Ablaufs. Und der Programmierer muss nicht alle möglichen und unmöglichen Kombinationen von Eingaben behandeln, sondern nur die, die möglich, weil sichtbar und somit bedienbar sind.

Das mag alles ein wenig theoretisch klingen. Doch im nächsten Kapitel werden Sie die Ablaufsteuerung mittels visuellen Objekten an einem ganz konkreten Beispiel verfolgen können...
X7.    ↑    Ablaufsteuerung Pizza-Plan


Wir haben im Theorie-Teil schon gehört, was mit Ablaufsteuerung bezweckt wird. Wir wenden uns also wieder unserem Pizza-Produktionsplan-Beispiel zu, um einen Teil der Ablaufsteuerungslogik unseres Programms in die Schaltung der visuellen Objekte zu legen: Zuerst soll ein Produkt in der Listbox gewählt werden, wobei direkt die Menge, die für die Dauer der Kampagne errechnet wird, in das Mengenfenster vorgetragen wird. Erst nach der Produktauswahl werden Optionsschalter für Kampagnen-Länge entweder nach Zeit-Vorgabe oder nach Mengenvorgabe angeboten. Steht der Optionbutton auf Zeitvorgabe, kann man in das Zeitfenster die Anzahl gewünschter Produktionstage eingeben. Vorgabewert ist bis zur nächsten Kampagne bzw. bis zum Monatsende, wenn noch keine Nachfolgekampagne bestimmt wurde. Verändert man diesen Vorgabewert, setzt das Exit-event des Eingabefelds ein und berechnet die daraus resultierende Menge.

Wählt man Mengenvorgabe, kann man in das Mengenfenster einen Wert eintragen. Als Vorgabewert steht dort der Wert, der sich aus der natürlichen Kampagnendauer bis zum nächsten Produktwechsel bzw. Monatsende ergibt. Verändert man diesen Wert, sollen die dafür erforderlichen ganzen Produktionstage ausgerechnet werden und dort eingetragen werden.

Erst mit dem Okay-Button wird die Kampagne eingetragen, sofern diese bestimmte Bereiche nicht überschreiten würde. Hierbei wird Umstellungsverlust bis zu 3 Tage und produktabhängige Kapazitätsausnutzung ("intens.") mitberücksichtigt; außerdem werden nachfolgende Kampagnen nach unten verschoben, falls die Anzahl der Produktionstage erhöht worden ist.

Oho! Ob wir uns damit nicht übernehmen werden?
Wir werden hier wohl nur einen Teil gemeinsam realisieren können - aber diesen sollten wir um so energischer angehen!
X8.    ↑    Pizza-Plan: Eingabefelder erst nach Listbox-Wahl zeigen


Optionsschalter und Optionale Eingabefelder sollen solange ausgeblendet bleiben, bis der User eine Listbox-Auswahl getroffen hat. Der User soll eben keine Angaben zu Anzahl Tagen machen und dann den Okay-Button betätigen, ohne nicht vorher ein Produkt ausgesucht zu haben!

Wir nehmen uns unser altes Beispiel vor, in dem wir ja bis zum Bau einer Listbox zur Auswahl einer Pizza-Sorte gekommen sind. Dies entspricht also dem Excel-Demo-File:
pizza03_listbox.xls
Hierzu schreiben wir in die cmdNewp_click-Routine des "plan"-Sheet-Moduls Anweisungen, 4 Objekte unsichtbar zu machen, bevor wir frmJob.Show anweisen:
Private Sub cmdNewP_Click()
frmJob.tbQuantperjob.Visible = False
frmJob.tbDays.Visible = False
frmJob.OptionButton1.Visible = False
frmJob.OptionButton2.Visible = False
frmJob.Show
End Sub
Und im frmJob-Modul zeigen wir ab jetzt in der ListBox_Click-Routine statt des Index alle ausgeblendeten Elemente, sofern die Wahl nicht auf den Titel statt auf eine Pizza-Sorte gefallen ist; in diesem Fall blenden wir die Elemente wieder aus:
Private Sub ListBox1_Click()
If frmJob.ListBox1.ListIndex > 0 Then
  'kann ich die nachfolgenden optionen sichtbar machen....
  frmJob.tbQuantperjob.Visible = True
  frmJob.tbDays.Visible = True
  frmJob.OptionButton1.Visible = True
  frmJob.OptionButton2.Visible = True
Else
  'wenn nur titelzeile gewählt wurde, lieber (wieder) nachfolgende
  'optionen unsichtbar machen....
  frmJob.tbQuantperjob.Visible = False
  frmJob.tbDays.Visible = False
  frmJob.OptionButton1.Visible = False
  frmJob.OptionButton2.Visible = False
End If
End Sub


Wenn wir das nun testen, erhalten wir je nach Auswahl folgende Zustände:

listboxoptionsoff listboxoptionson
X9.    ↑    Pizza-Plan: Kampagnenmenge bei Auswahl einer Sorte einblenden


Einen brennenden Wunsch zum Listbox1_Click: hätte ich allerdings noch:

Ich würde gerne beim Anklicken einer Pizza-Sorte die mögliche Produktionsmenge für den Abschnitt angezeigt bekommen, und zwar auch bei jedem weiteren Anklicken, noch bevor die Form mit Okay oder Cancel verlassen wird! - Wie kann man das realisieren?

Dazu gehört nicht all zu viel. Wir legen erst einmal eine Variable Co an, um dort die Spalte des Zellzeigers einzutragen:
Private Sub ListBox1_Click()
Dim Co As Long
Co = ActiveCell.Column
..
..
Dann tragen wir noch die Berechnung der Kampagne in die positive If-Folge ein:
If frmJob.ListBox1.ListIndex > 0 Then 
  .. (PNum ermitteln)
  ..
  .. (Objekte sichtbar schalten)
  ..
  'menge für ganze kampagne berechnen....
  [Quantperday] = main.calcQuantperday(Co, [PNum])
  [Daysforchange] = main.calcDaysforchange([PNum], [PreNum])
  [quantperjob] = ([Days] - [Daysforchange]) * [Quantperday]
  frmJob.tbQuantperjob = CInt([quantperjob])
Else
  ..
End If
Und das war es auch schon. Die neue eingefügten Zeilen bedeuten folgendes:

Die schon vorbereiteten Functions calcQuantperday und calcDaysforchange aufrufen und den Rückgabewert in ähnlich benannte Zellbereiche ablegen. Dann die Menge der ganzen Kampagne so berechnen und in die entsprechende Zelle und Textbox eintragen:
([Days] - [Daysforchange]) * [Quantperday]
Was soviel heißt wie: Kampagnengesamtmenge = (Kampagnendauer minus Umstellzeit) mal Tagesmenge.

Der komplette Listbox1_Click-Code lautet nun:
Private Sub ListBox1_Click()
Dim Co As Long
Co = ActiveCell.Column

If frmJob.ListBox1.ListIndex > 0 Then
  [PNum] = frmJob.ListBox1.ListIndex
  [PNam] = frmJob.ListBox1.Text
  'kann ich die nachfolgenden optionen sichtbar machen....
  frmJob.tbQuantperjob.Visible = True
  frmJob.tbDays.Visible = True
  frmJob.OptionButton1.Visible = True
  frmJob.OptionButton2.Visible = True
  
  'menge für ganze kampagne berechnen....
  [Quantperday] = main.calcQuantperday(Co, [PNum])
  [Daysforchange] = main.calcDaysforchange([PNum], [PreNum])
  [quantperjob] = ([Days] - [Daysforchange]) * [Quantperday]
  frmJob.tbQuantperjob = CInt([quantperjob])
Else
  'wenn nur titelzeile gewählt wurde, lieber (wieder) nachfolgende
  'optionen unsichtbar machen....
  frmJob.tbQuantperjob.Visible = False
  frmJob.tbDays.Visible = False
  frmJob.OptionButton1.Visible = False
  frmJob.OptionButton2.Visible = False
End If

End Sub

Fertig zum Testen!
X10.    ↑    Pizza-Plan: im newP-Schalter Form vorbereiten (Kampagnenlänge...)


Jetzt machen wir uns daran, die Vorgabewerte für die Ausgabe/Eingabe-Felder unserer Form vorzubereiten; die Form wird ja durch eine Click auf den cmdNewP-Button gestartet - und hier setzen wir also an. Noch bevor die Form aufgerufen wird ( frmJob.Show ), sollen also einige Dinge ausgerechnet und zur späteren Anzeige in entsprechende Felder vor-eingetragen werden.

Wir legen dafür zunächst einmal in der cmdNewP_Click-Sub eine ganze Reihe von Variablen an, die wir brauchen werden und puffern den Zeilenwert und Spaltenwert des Zellzeigers in Ro und Co, zusätzlich noch in oldRo (da sich Ro noch verändern wird, aber trotzdem noch auf den alten Wert von Ro zugegriffen werden soll); dann fragen wir nach dem korrekten Ausgangsort auf dem Excel-Sheet und springen andernfalls auf das Sub-Ende:
Dim Ro As Long, oldRo As Long, Co As Long, D As Long, v As Long, R2 As Long
Ro = ActiveCell.Row: Co = ActiveCell.Column: oldRo = Ro

'korrekter eingabe-bereich? .................
If Ro < 5 Or Ro > [maxro] Then GoTo mySubEnd
If Co <> 4 And Co <> 7 And Co <> 10 Then GoTo mySubEnd

Nun berechnen wir die Vorgabewerte für die Ausgabe/Eingabe-Felder unserer Form. Hierzu ermitteln wir erst einmal Namen und Nummer des Vorgänger-Produkts:
'vorgabewerte für userform berechnen.............
[Prenam] = main.findVorProd '...vorprod ermitteln
[PreNum] = main.findProdNum([Prenam]) '... produktnr. des Vorprods laut Liste
Die Anzahl an Tagen von der Zellzeigerzeile bis zum nächsten Produkt wird über die Funktion findNextProdRow ermittelt: der Rückgabewert minus der Ausgangszeile ist gleich der Anzahl Tage; diesen Wert speichern wir in der Zelle [daystonext]. Diese und viele andere Zellen müssen vorher angelegt worden sein in der Spalte "U" des "plan"-sheet; zum Aufbau der Bereichsnamen siehe Kapitel X1 - Bereichsnamen.

Zellen statt Variablen haben zwei Vorteile - gegenüber dem Nachteil, dass der Zugriff auf diese viel langsamer geht - : Man kann ihren Wert über einen Run hinaus erhalten und außerdem kann man sie wie auf einen Monitor betrachten, um zu Debuggen.
[daystonext] = main.findNextProdRow - Ro
  frmJob.lbDaystoNext.Caption = [daystonext]
  frmJob.tbDays.Value = [daystonext]
  [Days] = [daystonext]
Außerdem speichern wir die Anzahl Tage in frmJob.lbDaystoNext.Caption und in frmJob.tbDays.Value ab, wobei das erstere ein Label ist und seinen Wert in derCaption-Eigenschaft speichert und das letztere eine TextBox zur Eingabe ist, und wo Value für den Wert steht.

Ähnlich verfahren wir für die Anzahl Tag bis zum Monatsende; dieser Wert ist deswegen von Bedeutung, weil er die maximale Anzahl an Tagen bei extremer Kampagnenverlängerung durch den User bedeutet:
[daysToMonEnd] = [maxro] - Ro + 1 
  frmJob.lbDaystomonend.Caption = [daysToMonEnd]
Zu guter letzt ermitteln wir noch Namen und Nummer des nachfolgenden Produkts, indem wir auf den vorher schon ermittelten Wert [daystonext] zurückgreifen und die Werte in entsprechende Zellbereiche dauerhaft abspeichern:
[Nexnam] = Cells(Ro + [daystonext], Co)
[nexNum] = main.findProdNum([Nexnam])
Und für die Anzahl der zu verlängernden Tage setzen wir vorerst Null bzw. nichts ein:
[daystoprolong] = 0
  frmJob.lbDaystoprolong.Caption = ""
So, jetzt können wir das Ganze einmal testen:
Private Sub cmdNewP_Click()
Dim Ro As Long, oldRo As Long, Co As Long, D As Long, v As Long, R2 As Long
Ro = ActiveCell.Row: Co = ActiveCell.Column: oldRo = Ro

'korrekter eingabe-bereich? .................
If Ro < 5 Or Ro > [maxro] Then GoTo mySubend
If Co <> 4 And Co <> 7 And Co <> 10 Then GoTo mySubend

'vorgabewerte für userform berechnen.............
[Prenam] = main.findVorProd '...vorprod ermitteln
[PreNum] = main.findProdNum([Prenam]) '... produktnr. des Vorprods laut Liste
[daystonext] = main.findNextProdRow - Ro
  frmJob.lbDaystoNext.Caption = [daystonext]
  frmJob.tbDays.Value = [daystonext]
  [Days] = [daystonext]
[daysToMonEnd] = [maxro] - Ro + 1 '' später für variable Monatslänge erweitern!
  frmJob.lbDaystomonend.Caption = [daysToMonEnd]
[Nexnam] = Cells(Ro + [daystonext], Co)
[nexNum] = main.findProdNum([Nexnam])
[daystoprolong] = 0
  frmJob.lbDaystoprolong.Caption = ""

frmJob.tbQuantperjob.Visible = False
frmJob.tbDays.Visible = False
frmJob.OptionButton1.Visible = False
frmJob.OptionButton2.Visible = False
frmJob.Show
mySubend:
End Sub

X11.    ↑    Pizza-Plan: Tagesmengen einer Kampagne eintragen


Jetzt kommt der letzte Teil, den wir gemeinsam bearbeiten werden: Tagesmengen einer ganzen Kampagne einstellen, nachdem eine korrekte Wahl getrofen wurde. Wir werden dafür ein wenig zwischen den Modulen hin und her-springen. Passen Sie also bitte genau auf, wo wir gerade stehen, um nicht den Faden zu verlieren. Wenn Sie diesen Teil überstehen, wird alles nachfolgende wieder einfacher werden.

Falls Sie Probleme haben sollten, "mitzukommen", nehmen Sie einfach das mit, was Sie verstanden haben und üben Sie sich an selbtgestellten Aufgaben oder schauen Sie einmal bei den vielen anderen Demos 'rein, die man im Internet finden kann, wenn man seine Suchmaschine bemüht.

Auf gehts!

Im frmJob-Modul erweitern wir die Listbox1-Routine um folgenden Term bei korrekter Wahl einer Pizza-Sorte, damit Name und Nummer der aktuell einzufügenden Sorte als Zellvariable für andere Routinen zur Verfügung steht:
If frmJob.ListBox1.ListIndex > 0 Then
  [PNum] = frmJob.ListBox1.ListIndex
  [PNam] = frmJob.ListBox1.Text
  ..
  ..


Wir brauchen außerdem noch eine öffentliche Variable, auf die also von verschiedenen Modulen aus zugegriffen werden kann. Im main-Modul legen wir diese an. Sie bedeutet soviel wie, dass die Userform ohne korrekte Auswahl beendet wurde:
Public myCancelled As Boolean
Im frmJob-Modul legen wir endlich die Ereignisbehandlungsroutinen für cmdCancel und für cmdOk an:
Private Sub cmdCancel_Click()
frmJob.Hide
'myCancelled bleibt also auf startwert (true)
End Sub

Private Sub cmdOK_Click()
frmJob.Hide ' form auf jeden fall schon mal ausknipsen
If [Days] < [Daysforchange] Then
  MsgBox "Geht nicht - Umstellzeit wäre länger als diese Kampagne! "
  Exit Sub ' also so dass mycancelled immer noch auf true bleibt!
End If
If frmJob.ListBox1.ListIndex <= 0 Then Exit Sub
If [PNum] = [PreNum] Then Exit Sub
' erst wenn alle prüfungen überstanden, mycancelled auf false setzen....
myCancelled = False
End Sub

Dabei blenden wir in jedem Fall schon einmal die Form aus. In cmdOk_Click prüfen wir zusätzlich, ob die Umstellzeit überhaupt noch in die Kampagnen-Dauer passt - bei nein bleibt myCancel = True. Außerdem prüfen wir, ob es sich überhaupt um einen echten Wechsel handelt, oder ob hier vesucht wurde, mit sich selbst zu wechseln; wenn alles okay ist, dann erst wird die Public Variable myCancelled=False gesetzt.

Wir bauen jetzt in unserer alten Sub für cmdNewp_Click weitere Dinge ein: Wenn der User auf Cancel oder den Fenster-Schließer klickt, soll myCancelled = True gesetzt bleiben, und nur wenn Okay-Button betätigt wurde und die Auswahl okay ist (siehe einen Absatz höher), soll diese pessimistische Grundannahme zum Positiven verkehrt werden:

Vor dem frmJob.Show-Aufruf werden wir also die Variable auf True setzen und nach dem Aufruf werden wir fragen, ob sie immer noch True ist; wenn nicht, soll die ausgewählte Kampagne eingetragen werden, wenn ja, dann soll eben ans Prozedurende gesprungen werden, ohne die Kampagne einzutragen:
'userform aufrufen
myCancelled = True '.. form könnte ja auch durch window-close ohne cancel-button geschlossen werden
frmJob.Show
If myCancelled Then GoTo mySubEnd ''Exit Sub
'Kampagne eintragen.....
' ..
Und was alles im Einzelnen geschehen soll, wenn alles klar zum Eintrag ist, wird hier gezeigt:
'ggf Namen eintragen.......
If [PNam] <> [Prenam] Then
  Cells(Ro, Co) = [PNam] 'name nur wenn ungleich vorprod
Else
  Cells(Ro, Co) = "" ''sicherheitshalber löschen falls hier etwas übeschrieben wurde
End If

Call writeQuants(Ro, Co)

mySubEnd:
End Sub
Hiemit ergibt sich folgendes Konstrukt:
Private Sub cmdNewP_Click()
Dim Ro As Long, oldRo As Long, Co As Long, D As Long, v As Long, R2 As Long
Ro = ActiveCell.Row: Co = ActiveCell.Column: oldRo = Ro

'korrekter eingabe-bereich? .................
If Ro < 5 Or Ro > [maxro] Then GoTo mySubEnd
If Co <> 4 And Co <> 7 And Co <> 10 Then GoTo mySubEnd

'vorgabewerte für userform berechnen.............
[Prenam] = main.findVorProd '...vorprod ermitteln
[PreNum] = main.findProdNum([Prenam]) '... produktnr. des Vorprods laut Liste
[daystonext] = main.findNextProdRow - Ro
  frmJob.lbDaystoNext.Caption = [daystonext]
  frmJob.tbDays.Value = [daystonext]
  [Days] = [daystonext]
[daysToMonEnd] = [maxro] - Ro + 1 '' später für variable Monatslänge erweitern!
  frmJob.lbDaystomonend.Caption = [daysToMonEnd]
[Nexnam] = Cells(Ro + [daystonext], Co)
[nexNum] = main.findProdNum([Nexnam])
[daystoprolong] = 0
  frmJob.lbDaystoprolong.Caption = ""

frmJob.tbQuantperjob.Visible = False
frmJob.tbDays.Visible = False
frmJob.OptionButton1.Visible = False
frmJob.OptionButton2.Visible = False

 
'userform aufrufen
myCancelled = True '.. form könnte ja auch durch window-close ohne cancel-button geschlossen werden
frmJob.Show
If myCancelled Then GoTo mySubEnd ''Exit Sub

'Kampagne eintragen.....
'  ggf Namen eintragen.......
If [PNam] <> [Prenam] Then
  Cells(Ro, Co) = [PNam] 'name nur wenn ungleich vorprod
Else
  Cells(Ro, Co) = "" ''sicherheitshalber löschen falls hier etwas übeschrieben wurde
End If

'Mengen eintragen.....
Call writeQuants(Ro, Co)

mySubEnd:
End Sub



Testen Sie es gründlich! Es ist ja schon recht vertrackt gebaut!

Leider ist damit nur ein Teil der Möglichkeiten abgedeckt. Die Optionsschalter mit den Eingabefeldern für Mengenvorgaben durch den User oder Tagevorgaben sind noch nicht behandelt worden. Ich denke aber, dass die Beschreibung dieser Abläufe ein ganzes Buch füllen könnte und mache hier einfach 'mal Schluss!

Wer es ganz genau wissen will, kann ja das komplette File pizza06_move.xls, von dem im nächsten Kapitel die Rede sein wird, laden und analysieren.

Den bis hierher erarbeiteten Zwischenstand kann man hier vergleichen - wie immer laden, abspeichern und dann erst öffnen statt direkt vom Browser aus zu öffnen -
pizza05_logic.xls
X12.    ↑    Hinweise zu weiteren Funktionen


Man sollte mit den bisher erarbeiteten Routinen ein wenig herumexperimentieren und dann das komplette Demo betrachten - bitte laden, speichern und dann erst öffnen statt direkt-öffnen -
pizza06_move.xls
Für eine haargenaue Beschreibung jeder einzelnen Sequenz ist hier nicht der Platz. Einen gewissen Anhaltspunkt sollte in den Kommentar-Zeilen liegen und im Verhalten des Programms selber, wenn man die verschiedenen Objekte von allen Seiten testet.

Einige Anmerkungen will ich aber dennoch geben:

Die Verschiebe-Subs "schiebrauf" und "schiebrunter" sind nicht uninteressant in ihrem Aufbau. Schauen Sie sich doch einmal die Abfolge der Einzelheiten an. Hier komt auch die Copy & Paste von Excel-VBA zum Einsatz. Den Schiebrunter-Befehl verwende ich auch in der NewP-Routine, falls der User die vorgegebene Anzahl an Produktionstagen verlängert.

Die Monatsschalter ermitteln nur die Anzahl der Tage des aktuellen Monats mit der DateSerial-Function von VBA und tragen diese in der Zelle [days] ein; zusätzlich wir [maxro] errechnet aus [days]+4, um die höchstmögliche Zeile des aktuellen Monats anzuzeigen. Viele Routinen greifen auf dieses [MaxRo] zurück.

Ein Clear1-Vorgang kann zur Folge haben, dass das nachfolgende Produkt einen anderen Umstellungsverlust hat, da sich ja eine andere Kombination aus Produkt und nachfolgendem Produkt ergeben kann. Ich habe hier also die Routine entsprechend erweitert. Ebenso wird das nachfolgende Produkt bei einem NewP-Einfüge-Vorgang neu berechnet und nicht nur das aktuell eingefügte.

Ein Wechselschalter zum Ein/Aus-Schalten von automatischem Berechnen der Summen nach Einfügen oder Löschen einer Kampagne wird angelegt. Ich mache das aber erst einmal mit einem ganz einfachen CommandButton, der eine Zelle [autocalc] auf "ein" oder "aus" setzt, je nachdem, was er vorfindet. Bei cmdNewP_Click und cmdClear1_Click wird dann am Ende jeweils folgendes eingefügt:
If [plan!autocalc] = "ein" Then Call cmdCalc_Click


Ich könnte noch stundenlang weiter schreiben, was in den wenigen Hundert Zeilen steckt, aber mal ehrlich: Wollten Sie das alles lesen? Es ist an der Zeit, den SourceCode sprechen zu lassen!
 

 ↑  D. File & Error - Fehlerbehandlung und Systemfunktionen



D1a.   ↑    Error!


Ein dunkles Kapitel von VBA ist das der Fehlermeldungen. Sie fallen oft kryptisch, manchmal schlichtweg falsch aus! Fehlerhafte Fehlermeldungen kommen fast in allen Programmen vor, weil es einen erheblichen Aufwand bedeutet, allen Unwägbarkeiten, Fehlbedienungsmöglichkeiten und veränderten Grundlagen mit Abfangroutinen und Meldungen zu begegnen.

excel_errormsg.png

Wenn Sie so eine Dialogbox vorfinden und dann auf [Testen] klicken, können Sie im Debug-Modus (siehe weiter unten) fortfahren, um den Fehler aufzuspüren:

excel_errorline.png



D1b.   ↑    On Error Resume Next .. On Error Goto 0


Auch Sie selbst können dazu beitragen, Fehler abzufangen, sei es durch Eingabeprüfung bei Zahlenwerten, sei es bei Filezugriffsproblen etc. Sie können zB so vorgehen:
On Error Resume Next
ActiveWorkbook.SaveAs FileName:=fnam, FileFormat:= _
  xlWorkbookNormal, Password:="", CreateBackup:=False
If Err.Number <> 0 Then MsgBox ("kann Datei nicht schreiben: " + fnam)
On Error GoTo 0
Dabei bedeutet On Error Resume Next soviel wie: Wenn ab jetzt ein Fehler auftreten sollte, fahre bei der nächsten Zeile fort. Und diese Anweisung gilt bis zum Sub- oder Function--Ende, wenn nicht vorher mit On Error GoTo 0 - eine Null ist hier gemeint - diese Anweisung wieder außer Kraft gesetzt wird. Mit If Err.Number <> 0 Then .. kann man eigene Hinweistexte oder die Err.Description zeigen oder verzweigen zu besonderen Routinen etc.
D2a.   ↑    Debug (zur Kontrolle schrittweise ablaufen lassen)


Klicken Sie mit der linken Maustaste auf die linke Fensterleiste vom Sourcecode, um Haltepunkte zu setzen (und klicken Sie später noch einmal auf dieselbe Stelle um Haltepunkte zu enfernen). Nun starten Sie das Programm und prüfen Sie die Programmbestandteile, die Sie beim Ablauf interessieren, indem Sie sich zB mit dem Cursor über Variablen positionieren und warten, bis in einem kleinen gelben Fenster daneben der aktuelle Wert angezeigt wird. Benutzen Sie dann zum schrittweisen Weitermachen die F5-Taste.

Jetzt können Sie auch endlich das Direct-Fenster auf dem VBA-Bildschirm unten rechts verwenden. Schreiben Sie dort einfach ein Fragezeichen ("?") hinein und daneben den Ausdruck oder die Variable, dessen Wert Sie einsehen wollen + Enter-Taste. Sie können jetzt sogar Variablen einen bestimmten Wert zuweisen: a=3 +Enter-Taste und so experimentieren.

debug

D2b.   ↑    Kompilieren
Manchmal möchte man sich schon vorher davon überzeugen, dass alle Programmteile von Satzbau, Variablen und Objekten her korrekt geschrieben worden sind, bevor man sie in die Welt entlässt. Denn einige Fehler würden sonst erst zur Laufzeit angezeigt werden. Verwende ich zB eine Variable, die ich nicht richtig deklariert habe, tritt der Fehler nur dann auf, wenn mein Programmlauf dorthin gelangt. Gewisse Bedingungskonstallationen können so ausgelegt sein, dass dieser Programmteil fast nie durchgeführt wird. Dann wüsste man aber schon nicht mehr so genau, welcher Fehler hier gemeint sein könnte.

Und dem kann man mit der "Kompilieren" -Funktion entgegenwirken:

testcompile



D3.   ↑    Debugger -später- nicht vergessen auszuschalten!


Achtung!
Wundern Sie sich nicht, wenn Sie keine Buttons mehr ausführen können, solange der Debug-Mode noch aktiv ist ...
excel_debughold2.png Debug gestoppt
- jetzt können Sie wieder klicken ....
excel_debughold4.png
D4.   ↑    Der Macrorecorder und seine Grenzen


Der Makrorecorder ist ein sehr nützliches Element zum Aufzeichnen aller Bearbeitungsschritte und Eingaben (Excel-Menü: Extras / Makros / Aufzeichnen).

Viele Dinge, die wir gerne automatisieren wollen, lassen sich zwar aufzeichnen, funktionieren aber dann auch nur in genau demselben Zusammenhang. Wenn ich zB die Aufsummierung der Pizza-Tagesmengen aus den 3 Ofenstraßen automatisieren möchte, würde der Makro-Recorder zwar getreulich jeden Tastenschlag von mir aufzeichen - aber ich könnte den Code nur genau dann noch einmal verwenden, wenn sich exakt die gleiche Aufteilung des Produktionsplans ergeben würde; Wenn auch nur 1 Kampagne für einen Tag später eingegeben würde, würde mein aufgezeichneter Code ja schon daneben greifen und falsch rechnen. Wenn der Makro-Recorder uns jegliche Programmier-Arbeit abnehmen würde, würden wir uns ja auch erst gar nicht die Mühe machen müssen, in die VBA-Programmierung einzusteigen....

Es gibt aber immer noch viele wiederkehrende Aufgaben, die exakt in der gleichen Weise ablaufen - hier bietet sich also das Aufzeichnung der Bearbeitungsschritte per Makro-Recorder an. Wie ZB hier beim Vorbereiten eines Ausdrucks.

Schaut man sich dann den aufgezeichneten Code an (meistens in einem separaten Project-Modul abgelegt), ist man zunächst verblüfft über die Menge an Quelltext, die herbeigezaubert worden ist. Man erfährt auf diese Weise ganz einfach, wie die Funktionen und Objekte mit all ihren Einstellungen in VBA genannt werden. So weit so gut.

Wenn man aber genauer hinschaut, fällt einem auf, wie wortreich - um nicht zu sagen geschwätzig - diese Aufzeichnungen sind. Viel zu viel 'Zeug', das wir gar nicht eingestellt haben, wird in seiner Default-Einstellung mitaufgezählt. So haben wir nun das Problem, die interessanten Stellen nicht mehr davon unterscheiden zu können, wenn wir den vom Recorder gelieferten Code manuell überarbeiten müssen. Und überarbeiten müssen wir den Code häufiger, als uns lieb ist.

Wir sollten zB bei einer Makro-Aufzeichnung, mit der wir unsere Seite zum Drucken einrichten eine 600- dpi-Drucker-Einstellung, die mitaufgezeicnet wurde, wieder entfernen, da sich dieser Wert nur auf unseren eigenen Standard-Drucker bezieht. Andere User haben vielleicht nur einen 300-dpi-Drucker und bekommen nur 'Hieroglyphen' ausgedruckt, wenn diese Anweisung trotzdem bestehen bleibt!

Sehen Sie sich einmal die Makro-Aufzeichnung einer typischen Druck-Session an - man beachte den aufgeblähten Code:

Option Explicit

Sub DruckMakro_RecorderAufzeichnung()
'
' Makro3 Makro
' Makro am 18.05.2003 von a aufgezeichnet
'

'
    Application.Goto Reference:="druckplan"
    With ActiveSheet.PageSetup
        .LeftHeader = ""
        .CenterHeader = ""
        .RightHeader = ""
        .LeftFooter = ""
        .CenterFooter = ""
        .RightFooter = ""
        .LeftMargin = Application.InchesToPoints(0.787401575)
        .RightMargin = Application.InchesToPoints(0.787401575)
        .TopMargin = Application.InchesToPoints(0.984251969)
        .BottomMargin = Application.InchesToPoints(0.984251969)
        .HeaderMargin = Application.InchesToPoints(0.4921259845)
        .FooterMargin = Application.InchesToPoints(0.4921259845)
        .PrintHeadings = False
        .PrintGridlines = False
        .PrintComments = xlPrintNoComments
        .PrintQuality = 300
        .CenterHorizontally = False
        .CenterVertically = False
        .Orientation = xlPortrait
        .Draft = False
        .PaperSize = xlPaperA4
        .FirstPageNumber = xlAutomatic
        .Order = xlDownThenOver
        .BlackAndWhite = False
        .Zoom = False
        .FitToPagesWide = 1
        .FitToPagesTall = 1
    End With
    ActiveWindow.SelectedSheets.PrintPreview
    ActiveSheet.PageSetup.PrintArea = "$B$1:$M$35"
    With ActiveSheet.PageSetup
        .LeftHeader = ""
        .CenterHeader = ""
        .RightHeader = ""
        .LeftFooter = ""
        .CenterFooter = ""
        .RightFooter = ""
        .LeftMargin = Application.InchesToPoints(0.787401575)
        .RightMargin = Application.InchesToPoints(0.787401575)
        .TopMargin = Application.InchesToPoints(0.984251969)
        .BottomMargin = Application.InchesToPoints(0.984251969)
        .HeaderMargin = Application.InchesToPoints(0.4921259845)
        .FooterMargin = Application.InchesToPoints(0.4921259845)
        .PrintHeadings = False
        .PrintGridlines = False
        .PrintComments = xlPrintNoComments
        .PrintQuality = 300
        .CenterHorizontally = False
        .CenterVertically = False
        .Orientation = xlPortrait
        .Draft = False
        .PaperSize = xlPaperA4
        .FirstPageNumber = xlAutomatic
        .Order = xlDownThenOver
        .BlackAndWhite = False
        .Zoom = False
        .FitToPagesWide = 1
        .FitToPagesTall = 1
    End With
    ActiveWindow.SelectedSheets.PrintPreview
End Sub


D5.   ↑    Druck einrichten


Ich habe hingegen im Laufe der Zeit folgende knappe Druck-BefehlsSquenz in den meisten Fällen für ausreichend befunden:
Sub DruckPlan()
Sheets("Plan").Activate: [Plan!A1].Select
ActiveSheet.PageSetup.PrintArea = "Plan!B$2:$M$35"
ActiveSheet.PageSetup.Orientation = xlPortrait ' /xlLandscape (Hoch/Querformat)
ActiveSheet.PageSetup.FitToPagesWide = 1 ' alles an Seitenbreite anpassen
ActiveSheet.PageSetup.FitToPagesTall = 1 ' alles an Seitenhöhe anpassen
ActiveWindow.SelectedSheets.PrintOut Copies:=1, Collate:=True
End Sub
Übrigens, Druckseiten kann man ja eigentlich auch zuverlässig mit den entsprechenden Excel-Menübefehlen einrichten, ganz ohne jegliche VBA-Programmierung; Druckmakros sind nur dann interessant, wenn auf einem Sheet mehrere Bereiche zum Drucken vorbereitet werden sollen, so dass man also nicht einfach von der letzten Einstellung ausgehen kann, oder wenn man komplizierterer Vorbereitungen treffen will, wie zB bestimmte Werte für die Dauer der Druckersitzung ausblenden und dergleichen.
D6.   ↑    Zelllbereich in separate Datei exportieren


Hier habe ich eine Routine entwickelt zum Abspeichern von Zellbereichen als separates File, die als Excel-Funktion anscheinend fehlt. Um sie zu begreifen, sollte man in jede Zeile einen Anhaltepunk setzen (siehe Debug) und nachschauen, was gerade selektiert wird, welches File zusätzlich angelegt wird und wieder geschlossen wird, usw ...

Das Beispiel ist auch deswegen interessant, weil ich hier über mehrere Workbooks und Sheets gehe und außerdem Copy & Paste und Filesave einsetze.
Sub iExportPart(mySheetName As String, rangeName As String, fnam As String, FormelnAlsWerte As Variant)
' Aufgabe: save ein Range aus she=arbBlatt unter dem Namen fnam
' optional FormelNotVal: true=zellformeln als zellformeln - sonst als werte
Sheets(mySheetName).Activate
ThisWorkbook.Activate
Range(rangeName).Select
Selection.Copy
Workbooks.Add
[A1].Select
Sheets("tabelle1").Activate
If FormelnAlsWerte Then
  Selection.PasteSpecial Paste:=xlPasteValues
Else
   ActiveSheet.Paste
End If

On Error Resume Next
Kill fnam
On Error GoTo 0

On Error Resume Next
ActiveWorkbook.SaveAs FileName:=fnam, FileFormat:= _
  xlWorkbookNormal, Password:="", CreateBackup:=False
If Err.Number <> 0 Then MsgBox ("kann nicht schreiben: " + fnam)
On Error GoTo 0

ActiveWorkbook.Close savechanges:=False
[A1].Select 'um meldung zu vermeiden "es befinden sich große Datenmengen in Zwischenablage"
End Sub


und hier die aufrufende Routine zum testen:
Private Sub test_exp()
Call Util.iExportPart("change", "druckumstell", "test2.xls", True)
End Sub
 

Diverses





D7.   ↑    Quelltext (Sourcecode) verstecken - wenn es denn unbedingt sein muss...

vba-protect

D8.   ↑    Get Network User Name (angemeldeten Benutzernamen ermitteln)


Diese Function muss am Anfang eines Moduls stehen, wenn es funktionieren soll. Man kann damit den angemeldeten User ermitteln und dann je nach User bestimmte Funktionen erlauben oder sperrren oder Hinweise an bestimmte Personen geben etc.
br> Man beache bitte auch die Funktion UCase(), mit der man Strings in Großbuchstaben umwandeln kann, um so sicherzustellen, dass ein Stringvergleich nicht an Groß/Klein-Schreibunterschiede scheitern soll.
Declare Function GetNetworkUserName Lib "advapi32.dll" Alias "GetUserNameA" _ 
    (ByVal lpBuffer As String, nSize As Long) As Long
Function iGetNetUserName() As String
Dim MaxLen As Long, Result As Long, netUser As String
netUser = String$(254, 0)
MaxLen = 255
Result = GetNetworkUserName(netUser, MaxLen)
iGetNetUserName = Left$(netUser, MaxLen - 1)
'MsgBox iGetNetUserName
End Function

Sub test_netuser()
MsgBox iGetNetUserName
End Sub
D9.   ↑    weitere Funktionen kurz dargestellt (Copy & Paste, Screenupdating etc.)


Hier ist in knapper Darstellung ein Liste von Funktionen, die ich (vorerst) nicht breiter auswalzen möchte:

# Excel-Funktionen benutzen: zB WorksheetFunction.Round(17.3456, 3)

# ActiveCell.Row (ActiveCell.Column) : Zeilennummer (Spalten) der 
  aktuellen Zellzeigerposition

# Set myRange=[A1:D7] - Range einer Variable zuweisen

# With als Abkürzung für ein vollständigiges Objekt bei mehrfachem Zugriff,zB:
  With ActiveSheet.PageSetup
    .Orientation = xlPortrait
    .FitToPagesWide = 1
    .FitToPagesTall = 1
  End With

# Die letzte Spalte finden, für die in Zeile 1 Inhalt vorliegt:
  Range("IV1").Select
  Cols = Selection.End(xlToLeft).Column

# Copy & Paste - Beispiel:
  Sheets("Plan").Activate

  'a) copy.......
  Range(Cells(Ro, Co), Cells([maxro] - Dist, Co + 1)).Select
  Selection.Copy

  'b) zellzeiger woanders positionieren ...
  Cells(Ro + Dist, Co).Select

  'c) einfügen ''ActiveSheet.Paste
  Selection.PasteSpecial Paste:=xlValues 'nur werte, nicht aber formate

# Application.Wait = Now + TimeSerial(0, 0, 2) = 2 sec warten

# Unload me  me=object, indem prog gerade steht, zB Userform

# formXY.Repaint : Form refreshen (statt .hide und danach wieder .show)

# Application.DisplayAlerts = False : um Warnhinweise nicht zu zeigen

# Application.Calculation = xlCalculationManual 
  auf manuelle Zellberechnung stellen um Berechnungsablauf zu kontrollieren
  !!! Achtung unbedingt vorher folgenden perversen Term durchführen:
  ActiveSheet.Activate: Range(ActiveCell.Address).Select
  Achtung! Später wieder auf xlCalculationAutomatic setzen, sonst werden auch 
  andere Excel-Files mit abgeschalteter Zell-Neuberechnung gestartet!

# Application.ScreenUpdating = False   um Bild "einzufrieren"
  und somit enorme Beschleunigung zu erreichen
  und somit User-Verwirrung zu dämpfen

# Application.Visible = false um Excel zu verbergen - Vorsicht, Verwirrung vorprogrammiert!

# einem Makro einen ShortCut zuweisen, zB:
  Application.MacroOptions Macro:="pack", Description:= "",ShortcutKey:="c"

# Statusbar schreiben, zB: Application.StatusBar ="Berechnung läuft...." 
  Statusbar löschen : Application.StatusBar = False 

# Now : Datum und Uhrzeit von jetzt als Tagnummer seit 01.01.1900 mit Dezimalstellen für angebrochenen Tag 


	
Konvertierungen:

# S = Format(Now, "dd.mm.yyyy hh:mm:ss") Jetzt-Zeit formatieren 
  -  auch andere Formatangaben mögl. zB "dd.mm"

# m = 7 : A = DateSerial(2003, m, 1)  : Tagnummer zusammensetzen	

# Mid(S, 4, 2)	 : ab 4. Buchstaben 2 Buchstaben aus String S
	
# Len(S)	 : Länge eines Strings S

# S = "Hallo " & 123  : String plus anderen Ausdruck zusammenfügen

# A = CLng(" 123,6") : Ausdruck zu Long konvertieren (ergibt 124)

# A = Fix(123.6) :  zu Ganzzahl abschneiden (ergibt 123)

# A = CDbl(" 123,6") * 10 : Ausdruck zu Long (ergibt 1236)

# S = "abc" & cStr(123.456*2)  : Zahl zu String (ergibt "abc246.912")


 

 ↑  p. End & Links Zukunft von VBA, Links zu Userforen

p1.   ↑    VBA sinnvoll einsetzen


Je mehr man sich in Excel-VBA übt, um so mehr neigt man dazu, einfach alles in VBA zu erledigen, auch wenn es sich mit Zellformeln oft schneller verwirklichen lässt. Das Funktionsangebot der Excel-Zellformeln hat einen beachtlichen Umfang. Wenn man zu der Wenn-Funktion die Abfrage-Funktionen Summewenn, DBsumme, HVerweis usw. miteinbezieht, ergeben sich bei geschickter Kombination von mehreren Formeln ungeahnte Möglichkeiten.

Bei Datenbank-ähnlichen Aufgaben wird man hingegen etwas ausgebremst durch die 65536 Zeilen, die man maximal untereinander stehen haben kann.

Zellformeln arbeiten fast immer bedeutend schneller als entsprechende VBA-Programmierung. Wenn aber extrem viele Zellen mit Formeln belegt werden müssen, kann es sein, dass man hart an die Grenze der Leistungsfähigkeit des PC anlangt. Das merkt man dann zB, wenn ein manueller Kopierbefehl, der sich auf das ganze Sheet bezieht, sich scheinbar ins 'Nirwana' verabschiedet und nach einigen Minuten die Festplatte still zu swappen beginnt; oder man merkt es daran, dass man automatische Neuberechnung auf manuell umstellen muss, weil jeder Tastenschlag eine viertelminütliche Neuberechnungsphase zur Folge hat...

Kommt man also in solche Größenordnungen, kann VBA die bessere weil ressourcenschonendere Lösung sein. Durchläuft man immer wieder zum Suchen oder Zuordnen dieselben langen Zell-Listen, kann es gut möglich sein, dass man diese zeitweise in ein Array (Ram-Bereich) kopiert und intern absuchen lässt. Um das richtig einzuschätzen, ist gute Beobachtungsgabe des Laufzeitverhaltens gefragt und eine grobe Vorkalkulation, in welcher Größenordnung sich Zellzugriffe aus der geplanten Vorgehensweise ergeben werden.

Andererseits sollte man sich fragen, ob bei solchen Dimensionen eine echte Datenbank-Lösung mit geregeltem Multi-User-Verkehr nicht eher in Frage kommt. Auch von Excel aus lässt sich da sehr viel machen: Die mitgelieferte Windows-ADO-Bibliothek ist im Prinzip für jeden Zugriff auf SQL-fähige Datenbanken via ODBC geeignet. Außerdem steht der direkte Zugriff auf die MS-Access-Kernfunktionen zur Verfügung!

Access ist ein kleines Datenbank-Programm mit SQL-Abfragbarkeit und beliebtem Userinterface, das gerne für kleinere Standalone-Unternehmensanwendungen wie zB Stoffdatenbanken oder Personaleinsatzplanung eingesetzt wird oder als Frontend für den Zugriff auf echte Datenbanksysteme wie Oracle, DB2 oder MS-SQL in Echtzeit, ohne aber über die enormen Wandlungsmöglichkeiten der Excel-Zellformeln zu verfügen .

Excel ist die ideale Plattform für gemischten Datenverkehr, vor allem wenn es darum geht, Datenfomate mit VBA-Funktionen zu wandeln oder aufwendige Reports zu gestalten, die jeden noch so speziellen Wunsch erfüllen !

Auch für die Datenerfassung ist Excel an sich besonders gut geeeignet, da sich die Berechnung der eingegeben Werte sofort realisieren lässt und schnell und einfach an geänderte Vorgaben anpassen lässt, ohne Experten zu Rate ziehen zu müssen. Die vorberechneten Werte können dann per ADO und ODBC auf Datenbanken übertragen werden, wofür VBA-Programmierungen erforderlich werden.

Zahlreiche Datenbankabfrageprogramme und Datendienste bieten eine direkte Schnittstelle zu Excel an, damit man dort auf seine eigene Art Daten nachbearbeiten und weitergeben kann. Um all diese Vorgänge in Stufen und in einer ganz bestimmten Reihenfolge gezielt ablaufen zu lassen (also nicht einfach durch permanentes direktes Neuberechnen von allen Zellformeln), um zu kontrollieren und zu automatisieren - all dafür ist unsere VBA-Programmierung hervorragend geeignet!

Auch in den Ursprüngen von Excel-VBA liegt auch heute noch eines der Hauptanwendungsgebiete: Dann, wenn man immer wieder dieselben Programm-Bearbeitungsschritte durchführt, zeichnet man diese per Makro-Recorder auf und lässt die ganze Serie dann auf Wunsch wieder ablaufen. Aus der Erweiterung dieser Befehlsstapel zu einer echten Programmiersprache wurde dann später VBA entwickelt. Ein typisches Beispiel für einen solchen Tastatur-Makro-Ablauf ist das Beispiel Export Zellbereich als eigene Datei, für das ich hier eine Funktion geschrieben habe, die fast nur aus überarbeiteten Makro-Recorder-Aufzeichnung besteht. In den gleichen Zusammenhang sind auch Druck-Makros, Buttons zur Einstellung eines Zeitbereichs und Navigationshilfen zu sehen.

Der Einsatz von VBA als Programmiersprache bringt noch weitere Vorteile mit sich:
p2.   ↑    Ausblick


Microsoft hat seine User schon öfter durch radikale Versionswechsel mit mangelnder Abwärtskompatibilität verschreckt; so zB sind frühere Versionen in deutscher Befehlssprache gehalten und zum Konvertieren in die neuen englischsprachigen Versionen ungeeignet gewesen. Wenn da jemand viel Zeit hineingesteckt hat, wird er nicht gerade darüber erfreut sein, alles noch einmal umzuschreiben; daher ist die besorgte Frage nach der Beständigkeit des heute entwickelten Codes von großer Bedeutung!

Microsoft setzt zur Zeit (2001-2003) alles auf die neue .Net-Strategie (sprich 'DotNet'), um alle Geräte, Dienste, Programme und Programmierumgebungen auf einen gemeinsamen Nenner zu bringen. Dies ist die Antwort auf die allesumfassende Java-Initiative der 'Gegenseite'. Ob diese Strategie Erfolg haben wird, ist noch ungewiss - MS jedenfalls zertifiziert seine Prüfungskandidaten jetzt schon nur noch auf dieses neue System mit C# (C-sharp) als Flaggschiff unter den Programmiersprachen, unter denen aber auch VB.Net eine tragende Rolle spielt, wohingegen so bewährte Sprachen wie C++ nur noch am Rande vorkommen.

Es sieht im Moment so aus, als wenn es innerhalb der Office-Applications vorerst bei VBA bleiben wird, mit einigen Erweiterungen in Richtung Datenaustauschbarkeit per XML-Fomat. Hingegen gibt es schon als eine Art von zweitem Weg die Möglichkeit, von VB.Net oder C#.Net aus auf Kern-Funktionen von Word, Excel und Konsorten zuzugreifen, was mit Office.Net bezeichnet wird.

Wenn nun Excel radikal auf .Net umgestellt würde - was sich wie gesagt im Moment nicht anzudeuten scheint - wären einerseits wieder massive Abwärtskompatibilitätsprobleme zu befürchten - andererseits würde man mit seinen dann zu erweiternden .Net-Sprachkenntnissen so ziemlich alles programmieren können, was nur irgendwie digital ist: von der Foto-Camera, über Büroprogramme und eigenständige PC-Programme bis hin zu Web- und anderen Servern...

Doch lassen Sie sich von solchen Spekulationen erst einmal nicht irritieren und üben Sie sich weiter in der VBA-Programmierung, denn keine Umstellung kann so radikal sein, dass Sie Ihre frisch erworbenen Grundkenntnisse von Schleifen und Objekten vollends über Bord werfen müssen!
Und jetzt wünsche Ihnen noch viel Freude an Ihren weiteren Excel-Exkursionen !


Andreas Klotz zu Köln, im Mai 2003
p3.   ↑    Demo-Download und Sicherheit
Sie haben vielleicht schon eimal davon gehört, dass es sogenannte Makroviren gibt. Es gehört nicht all zu viel dazu, gefährliche Operationen wie das Löschen von Dateien auf der Festplatte von Excel aus mit automatischem Makro-Start ablaufen zu lassen. Kaputt-machen war immer schon einfacher als aufbauen und auch für die Technik zur Ausbreitung von Viren muss man nicht gerade ein Genie sein. Gerade auch Office-Dokumente bleiben davon nicht verschont. Daher gibt es die entsprechende Hinweis-Meldung von Excel, die vor dem Ausführen von Makros warnt; ich rate Ihnen, diesen Makro-Warnhinweis-Dienst niemals abzuschalten - und wenn er noch so lästig ist - um gedankenloses Öffnen von verseuchten Makro-Dateien zu vermeiden.

Wenn Sie sich also nicht sicher sind, ob diese meine Beispielsdateien okay sind, können Sie aber zumindest die erste Datei für das große Pizza-Demo, in der noch keine Makros enthalten sind, laden und auf Ihre Festplatte speichern, um sich langweilige Tipparbeit beim Aufbau von Beispielsmaterial zu ersparen: pizza01_purecells.xls
Wenn Ihnen auch das noch zu unsicher ist, dann machen Sie sich halt für eine Vierstelstunde die Mühe, in möglichst demselben Aufbau ähnliche Beispielstabellen anzulegen. Orientieren Sie sich hierzu an den Abbildungen im Kapitel X1: "Plan"-Arbeitsblatt und "Change"-Arbeitsblatt. und vergessen Sie auch nicht, die Bereichsnamen für bestimmte Zellen zu vergeben.

Sie sollten eigentlich von dort an alles, was in diesem Kurs besprochen wird, von Grund auf selber aufbauen! Wenn Sie aber absolut keine Lust dazu haben und darauf vertrauen, dass diese Quelle hier virenfrei ist, können Sie auch diese Excel-File-Sammlung hier laden: pizza_demo_excel_files.zip 0,2 MB und die dort enthaltenen Beispielsdateien in ein lokales Arbeitsverzeichnis Ihrer Wahl entpacken, z.B. "C:\Programme\ExcelDemo", (vorher anlegen). Sie sollten die entsprechenden Excel-Files nicht direkt vom Browser aus starten! Denn dann arbeitet die VBA-Entwicklungsumgebung nicht mehr 100% korrekt.

Folgende 9 Excel-Demo-Files sind darin enthalten:

cellspeedtest.xls loop.xls othereventtypes.xls
pizza01_purecells.xls pizza02_calcsum.xls pizza03_find.xls
pizza04_listbox.xls pizza05_logic.xls pizza06_move.xls
p4.   ↑    Das komplette Listing von pizza05_logic.xls


Hier folgt nun der Sourcecode vom Programm pizza05_logic.xls, also nicht vom fertigen Programm (pizza06_move.xls), sondern so weit, wie wir ihn zusammen besprochen haben. Der Text ist innerhalb des Excel-Files mehreren Modulen zugeordnet: "Plan"-sheet-Modul, "Main"-Modul und "frmJob"-Modul:
pizza05_plan.txt
pizza05_main.txt
pizza05_frmjob.txt
p5.   ↑    Das komplette Listing von pizza06_move.xls


Hier folgt nun der Sourcecode vom Programm pizza06_move.xls. Dies ist das vorerst fertige Programm, das viele Funktionen zusätzlich zu denen enthält, die wir besprochen haben. Der Text ist innerhalb des Excel-Files mehreren Modulen zugeordnet: "Plan"-sheet-Modul, "Main"-Modul, "frmJob"-Modul und "util"-Modul:
pizza06_plan.txt
pizza06_main.txt
pizza06_frmjob.txt
pizza06_util.txt
p6.   ↑    Links


HERBER.de      SPOTLIGHT.de

Ende.