Gut zu Wissen « Bernhard Krenz

Gut zu Wissen

Kostenlose Zeiterfassung online unter http://www.timerdesk.de

Unter http://www.timerdesk.de findet sich ein von mir entwickeltes Programm zur Zeiterfassung. Timerdesk ist bewusst schlicht in Funktionen, Farben und Design gehalten. Die Konzentration auf einfache Datenverwaltung und nützliche Berichte stehen im Mittelpunkt.

Die Zeiterfassung erfolgt nach den Wünschen der Nutzer. Neben der reinen Erfassung eigener Aufgaben können diese Projekten, Aufträgen oder Kunden zugewiesen werden. Darüber hinaus können andere Nutzer eingeladen werden, um gemeinsam an Projekten zu arbeiten und um die Aufwände in den Berichten zu erfassen.

Weitere Funktionen des Zeiterfassung-Programms sind eine Stundenliste nach Aufgaben pro Kunde zur Beschleunigung der Rechnungslegung sowie eine Übersicht der eigenen Arbeitszeiten zur Analyse und Verbesserung.

Timerdesk ist komplett kostenfrei ohne versteckte Kosten oder Sonder-Module.


Objective-C weak-Eigenschaft aus Swift

Mit der aktuellen Swift-Version scheint es nicht möglich zu sein, einer weak-Eigenschaft einer Objective-C-Klasse einen Wert zuzuweisen. Auch mit einem manuellen setter ist die Variable immer nil.

Dies betrifft vor allem den Fall, in dem in einem Swift-Projekt eine Objective-C Bibliothek genutzt wird, die zum Beispiel ein weak delegate besetzt.

@property (weak) id<Protocol> dataSource;

Die einzige Lösung bestand darin, die weak-Eigenschaft in eine strong-Eigenschaft zu ändern. Ich hoffe, dass Apple dafür einen Fix liefert.


File.Move nach File.Delete

Die Windows-Welt hält doch immer wieder Überraschungen bereit. Gerade brachten mich folgende Code-Zeilen zur Verzweiflung:

if (File.Exists(filePath + ".backup"))
{
	File.Delete(filePath + ".backup");
}

if (File.Exists(filePath))
{
	File.Move(filePath, filePath + ".backup");
}

Ziel des Ganzen: Eine Datei sollte vor dem Überschreiben in eine backup-Datei gespeichert werden. Existierte diese backup-Datei bereits, wird sie vorher gelöscht.

Der Code erzeugte eine IOException mit der Meldung, dass die Datei nicht bewegt werden konnte, da diese bereits existiert. Mit dem oberen Code natürlich völlig unmöglich. Eine Recherche zeigte als Ursache, dass das File.Delete möglicherweise zu lange dauert und zum Zeitpunkt des File.Move noch existierte. Für mich ist dann fraglich, warum der Code nach File.Delete ausgeführt wird. Dies sollt erst geschehen, wenn die Datei tatsächlich gelöscht wurde.

Wie dem auch sei. Folgende Änderung vermeidet den Fehler:

if (File.Exists(filePath))
{
	File.Copy(filePath, filePath + ".backup", true);
}

Hiermit wird die aktuelle Datei-Version zum Backup kopiert und überschrieben, falls diese bereits vorhanden ist.

Im nächsten Schritt muss natürlich sicher gestellt werden, dass die aktuelle Datei-Version durch die Neue überschrieben wird.


Falsche Bild-Größen in WPF

Als WPF-Entwickler, der hin und wieder mit einem Designer zusammen arbeitet, wird man früher oder später mit dem Problem konfrontiert, dass die zugelieferten Grafiken in der Anwendung größer dargestellt werden, als sie abgespeichert wurden.

Dies wird immer dann auftreten, wenn ein Control die Größe des darin enthaltenen Bildes annehmen soll, zum Beispiel ein Button.

Ursache hierfür ist ziemlich sicher, dass der Designer die Grafiken mit einem Apple-Mac erstellt hat. Dieser arbeitet standardmäßig mit 72dpi für die Grafiken, während Microsoft in Windows 96dpi verwendet. Wird das Bild in WPF dargestellt, wird es um den Faktor 96/72 = 1,33 größer dargestellt.
Eine relativ schlechte Lösung wäre, die Bild-Größen statisch im Code zu hinterlegen. Spätere Größenänderung des Bildes müssten dann jedes Mal zusätzlich auch im Code nachgearbeitet werden.

Da ich in meinem Projekten recht häufig mit solchen Konstellationen arbeite, habe ich ein kleines UserControl entwickelt, dass sich diesem Problem annimmt. Beim Öffnen des Bildes wird die Orginalgröße ausgelesen und dem Bild zugewiesen. Der DPI-Wert wird ignoriert.

Das UserControl arbeitet aktuell nur mit einem BitmapSource als Source des Bildes, zum Beispiel eine Pfad zu einem Bild.

Folgend der Code für das Control:

public class DPIAwareImage : Image
{
	static DPIAwareImage()
	{
		// listen to changes to stretch property, and set default value to fill
		StretchProperty.OverrideMetadata(typeof(DPIAwareImage), new FrameworkPropertyMetadata(Stretch.Fill, new PropertyChangedCallback(StretchPropertyChanged)));
		/// listen to changes to source property
		SourceProperty.OverrideMetadata(typeof(DPIAwareImage), new FrameworkPropertyMetadata(new PropertyChangedCallback(SourcePropertyChanged)));
	}

	// check if a Stretch other then Fill is set.
	private static void StretchPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
	{
		if (args.NewValue == null)
		{
			return;
		}

		if ((Stretch)args.NewValue != Stretch.Fill)
		{
			throw new NotSupportedException("DPIAwareImage only works with Stretch=Fill.");
		}
	}

	// check the new image source and set the origin width and height if available
	private static void SourcePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
	{
		var source = args.NewValue as BitmapSource;

		if (source == null)
		{
			return;
		}

		var image = sender as DPIAwareImage;

		image.Stretch = Stretch.Fill;
		image.Width = source.PixelWidth;
		image.Height = source.PixelHeight;
	}
}

In den Zeilen 38 und 39 wird die Orginalgröße der Datenquelle ausgelesen und dem Bild zugewiesen.

Das Control überwacht neben Änderungen der Source-Eigenschaft auch die Stretch-Eigenschaft. Alles außer „Fill“ würde für den Verwendungszweck nicht funktionieren, da sich der möglicherweise „falsche“ DPI-Inhalt an die festgelegte Größe skalieren muss.


Länge/Dauer eines Videos mit MediaInfo ermitteln

Wer versucht die Länge eines Videos C# zu ermitteln, wird im Internet viele Lösungsvorschläge finden. Oftmals sind diese aber unvollständig, funktionieren nicht oder einfach nur unverständlich.

Ich zeige folgend die Nutzung der MediaInfo-Bibliothek.

Vorbereitung:

  1. Die aktuelle Version der MediaInfo-Bibliothek herunterladen
  2. Die MediaInfo.dll selbst, als auch den C# Wrapper in das eigene Projekt integrieren
  3. Die Video-Datei mit Hilfe des Wrappers öffnen, die Länge abfragen und wieder schließen

Download der MediaInfo-Bibliothek

  1. Auf der SourceForge-Seite den Reiter „Files“ auswählen
  2. In der Verzeichnisliste „binary“ auswählen
  3. „libmediainfo0“ auswählen
  4. Den obersten Eintrag/die aktuellste Version auswählen (z.B. „0.7.62“)
  5. Die passende Windows-Version „WithoutInstaller“ downloaden. Für ein 64bit System muss die x64 Version heruntergeladen werden, für ein 32bit die i386
    (z.B. „MediaInfo_DLL_0.7.62_Windows_x64_WithoutInstaller.7z“

Projektintegration

  1. Die heruntegeladene 7z-Datei mit 7-Zip entpacken
  2. Die MediaInfo.dll aus dem Hauptverzeichnis in das Visual-Studio-Projekt auf der Hauptebene integrieren
  3. Im Eigenschaftsfenster der MediaInfo.dll den „BuildVorgang“ auf „Inhalt“ und „In Ausgabeverzeichnis kopieren“ auf „Kopieren, wenn neuer“ stellen
    (Die MediaInfo.dll muss später parallel zur exe der Anwendung liegen.)
  4. In der Zip unter „Developers“->“Source“->“MediaInfoDll“ die „MediaInfoDLL.cs“ (C# Wrapper) an eine beliebige Stelle in das Visual-Studio-Projekt integrieren

Länge der Video-Datei abfragen

var mediaInfo = new MediaInfo();
mediaInfo.Open(fullPath);
var durationInMilliSecondsString = mediaInfo.Get(StreamKind.Video, 0, "Duration");
mediaInfo.Close();

if (durationInMilliSecondsString != null)
{
	var durationInMilliseconds = long.Parse(durationInMilliSecondsString);
}

Offene Verbindungen zu einer SQL-Datenbank trennen

Ab und zu steht man vor der Herausforderung eine SQL-Datenbank wiederherzustellen oder sonstwie zurück zu setzen. Dann wird man schnell mit dem Problem konfrontiert, dass offene Verbindung zur Datenbank diese Aufgabe verhindern.

Nun kann man entweder mit den üblichen Tools, wie dem SQL-Management-Studio, die Verbindungen trennen, oder man bastelt sich dazu ein kleines Skript. Da ich selber hauptsächlich skriptbasiert arbeite, habe ich dafür folgenden Algorithmus entwickelt:

USE master

DECLARE @databaseName VARCHAR(15)
SET @databaseName = 'name_of_your_database'

DECLARE @kill_id INT
DECLARE @disconnectQuery NVARCHAR(255)
DECLARE killprocess_cursor CURSOR FOR SELECT a.spid FROM sysprocesses a JOIN sysdatabases b ON a.dbid = b.dbid WHERE b.name = @databaseName
OPEN killprocess_cursor

FETCH NEXT FROM killprocess_cursor INTO @kill_id
WHILE @@FETCH_STATUS = 0
BEGIN
	SET @disconnectQuery = 'KILL '+ CONVERT(VARCHAR, @kill_id)
	RAISERROR ('EXECUTING: %s', 1, 1, @disconnectQuery) WITH NOWAIT
	EXEC (@disconnectQuery)
	FETCH NEXT FROM killprocess_cursor INTO @kill_id
END	

CLOSE killprocess_cursor
DEALLOCATE killprocess_cursor

Das Skript durchläuft mit einem Cursor sämtliche offenen Prozesse der festgelegten Datenbank und schließt diese mit Hilfe des „Kill“-Befehls und der zugehörigen Prozess-Id.


if (![nil isEqualToString:nil]) { }

Das folgende Verhalten entspricht der Apple’s Objective-C 2.0 Dokumentation, ist aber möglicherweise nicht jedermann bekannt.

Bei einem Vergleich zweiter Strings in iOS mit isEqualToString, wird das „do something“ im folgenden Codeschnipsel auch ausgeführt, wenn beide Argumente nil sind, obwohl diese auf den ersten Blick gleich erscheinen.

if (![string1 isEqualToString:string2])
{
 // do something
}

Das liegt daran, dass eine Methode zu nil immer 0 zurückgibt und als false interpretiert wird. Die if-Bedingung ist damit erfüllt.

Die korrekt Art und Weise das obere Beispiel umzusetzen, wäre also:

if (![string1 isEqualToString:string2] && (string1 != nil || string2 != nil))
{
  // do something
}

Dateiupload mit Windows-Phone 8

Wer versuch mit der API von Windows-Phone (7, 7.5, 8) eine Datei auf einem Webserver hochzuladen, wird schnell merken, dass die einfach zu nutzende WebClient-Klasse hierfür keine Funktionen zur Verfügung stellt.

Wer keine Lust hat in den Tiefen des HttpWebRequest zu irren, der nutzt dazu am Besten vorhandene Bibliotheken der Community. Ein Dateiupload lässt sich zum Beispiel mit RestSharp realisieren.

In meinem Beispiel will ich eine Datei aus einem bestimmten Verzeichnis des IsolatedStorage hochladen.

Schritt 1: Ermittlung des Dateipfades

var directoryName = "FilesToUpload";

string[] files = null;

// get all files from the isolatedstorage in the specified directory
using (var storage = IsolatedStorageFile.GetUserStoreForApplication())
{
	// if the directory does not exists, there is nothing to do
	if (!storage.DirectoryExists(directoryName))
	{
		return;
	}

	files = storage.GetFileNames(Path.Combine(directoryName, "*.xml"));
}

// if there is no file present in the directory,
// there is nothing to do either
if (files == null || files.Length == 0)
{
	return;
}

// use the first file for upload
var filePath = Path.Combine(directoryName, files[0]);

Schritt 2: Formulierung des Webrequests

(continue reading…)


Falsche Größe eines ImageView in einem ListView

Wer in einem Android-ListView Reihen mit unterschiedlicher Höhe hat, zum Beispiel Bilder und Text-Elemente, wird eventuell wie ich auf das Problem stoßen, dass die Reihe für das Bild mit einer falschen Höhe angezeigt wird.

In meinem Fall war diese zu hoch, was je nach Skalierungsverhalten des Bildes entweder zu einer Verzerrung des Bildes oder zu einem Abstand zur nächsten Reihe führte.

Im Adapter des ListViews muss deshalb zur Laufzeit die Höhe des Bildes berechnet und manuell gesetzt werden. Der View dazu wird aus einer Layout-XML in res/layout erzeugt.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	android:orientation="vertical">

	<ImageView
		android:id="@+id/imageView"
		android:layout_width="match_parent"
		android:layout_height="wrap_content"
		android:scaleType="fitXY"
		/>

</LinearLayout>

Die getView-Methode im Adapter könnte wie folgt aussehen. Dies ist natürlich nur der Auszug für den ImageView. Für die anderen Reihen gibt es je nach Position anderen Inhalt.

@Override
public View getView(int position, View convertView, ViewGroup parent)
{
	View view = (LinearLayout) mInflater.inflate(R.layout.image_item, null);
	
	ImageView imageView = (ImageView) view.findViewById(R.id.imageView);

	// load the bitmap for the image source
	String filePath = "xyz"; // you get this from somewhere, depending on the position	
	Bitmap bitmap = Util.loadBitmap((Activity) context, RessourceLoader.getFilesDirectory() + filePath);
	
	if (bitmap != null)
	{
		imageView.setImageBitmap(bitmap);

		// get the dimensions of the current display
		DisplayMetrics dm = new DisplayMetrics();
		((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(dm);
		if (dm != null)
		{
			// calculate height and set it via layout params
			float height = ((float) dm.widthPixels / bitmap.getWidth()) * bitmap.getHeight();
			imageView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, (int)height));
		}
	}
	
	return view;
}

SQL-Skriptdateien aus Masterskript aufrufen

Bei der Architektur einer Datenbank gilt es nach einer Testphase oder nach Änderungen oftmals diese komplett neu aus den beteiligten Skripten zu erzeugen. Um zu viel manuelle Arbeit zu vermeiden erzeuge ich mir dazu ein weiteres Skript, welches neben der Installation der Datenbank auch alle beteiligten Skripte in fest definierter Reihenfolge ausführt.

Das Ausführen der Skripte aus aus einer Datei erfolgt über den Befehl xp_cmdshell. Dieser macht nichts weiter, als einen Befehl an die Windows-Kommandozeile weiter zu erreichen. Der Befehl ist im Standardfall allerdings sicherheitsbeschränkt, weshalb die Konfiguration vorher geändert werden muss.

Folgende Schritte sind dazu notwendig:

  1. Löschen der Datenbank falls diese existiert
  2. Datenbank neu erstellen
  3. Berechtigung konfigurieren, um Skripte aus Dateien auszuführen
  4. Konstanten deklarieren und initialisieren
  5. Skript-Dateien in eine temporäre Tabelle einfügen
  6. Diese Tabelle in einer Schleife durchlaufen und die Skripte aus den Dateien ausführen

Sehen wir uns einige der Schritte im Detail an:

Schritt 3:

-- To allow advanced options to be changed.
EXEC sp_configure 'show advanced options', 1

GO

-- To update the currently configured value for advanced options.
RECONFIGURE

GO

-- To enable the feature.
EXEC sp_configure 'xp_cmdshell', 1

GO

-- To update the currently configured value for this feature.
RECONFIGURE

Mit diesem Schnipsel werden die erweiterten Berechtigungen für den xp_cmdshell-Befehl gesetzt.

(continue reading…)


Copyright © 2012-2019 Bernhard Krenz Alle Rechte vorbehalten.