Модераторы: gambit, Kefir, Partizan

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> Бегущая строка, реализация 
:(
    Опции темы
AleksPingvin
  Дата 24.5.2011, 07:00 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 62
Регистрация: 20.5.2011

Репутация: нет
Всего: нет



Доброе утро, друзья!

Продолжаю задавать глупые вопросы по WPF..(предварительно разумеется гуглю).
Хотелось бы услышать ваше профессиональное мнение по реализации одного компонента.

Согласно поставленной мне задаче в программе должна быть бегущая строка. 
По скольку она должна быть довольно сильно стилизована (сложный фон, двигается не только текст но и пиктограммы), то я решил делать это следующим образом. Создал свой класс CreepLine унаследовав его от Canvas, объявил нужные DependencyProperty, перегрузил GetVisualChild, VisualChildrenCount. 
Объявил объект DrawingVisual, который добавляю на канву через AddVisualChild и AddLogicalChild, на нем я и хочу рисовать все что мне надо. 
Далее я хочу отрендерить данные и clip-ать их по нужному прямоугольнику. А в анимации менять свойство которое будет отвечать за смещение прямоугольника обрезки.

Вопрос на сколько оправдана такая реализация и вообще возможно ли она? Не будет ли проблем с производительностью?
Быть может есть более "гуманный" способ реализации бегущей строки? К сожалению в интернете я ни где не смог найти реализации бегущей строки на wpf, разве что только в текстовом поле DataGrid, но мне это ведь не катит...

С Уважением,
Александр
PM MAIL   Вверх
-Mikle-
Дата 24.5.2011, 13:17 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Невидимка Vingrad'а
***


Профиль
Группа: Экс. модератор
Сообщений: 1672
Регистрация: 22.6.2003
Где: Казахстан, Астана

Репутация: 13
Всего: 59



Цитата(AleksPingvin @  24.5.2011,  10:00 Найти цитируемый пост)
К сожалению в интернете я ни где не смог найти реализации бегущей строки на wpf

Я тоже реально толкового не нашел, поэтому сделал свой. Код выложить не могу (права конторы), но опишу использованную идею, как время будет.


--------------------
Если тебе плюют в спину, значит ты впереди...
PM   Вверх
AleksPingvin
Дата 24.5.2011, 13:48 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 62
Регистрация: 20.5.2011

Репутация: нет
Всего: нет



Буду ждать с нетерпением.
PM MAIL   Вверх
AleksPingvin
Дата 24.5.2011, 21:16 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 62
Регистрация: 20.5.2011

Репутация: нет
Всего: нет



Кстати попробовал реализовать идею и неожиданно столкнулся с проблемой создания пустого изображения. Хочу сделать следующее:
1) Рассчитать ширину изображения, путем сложения ширины текста (FormattedText мне в помощь) и пиктограм + margin.
2) Создать изображение и отрисовать на нем текст и строку
3) Согласно текущему смещению отрендерить путем Clipin-ga в методе OnRender на мой компонент .

Вот ни как не найду нужные классы для пункта 2.. не подскажете ли? Видел пример с использованием Canvas, которая потом подается в RenderTargetBitmap , но что-то мне подсказывает, что это не правильный путь....

Это сообщение отредактировал(а) AleksPingvin - 24.5.2011, 21:19
PM MAIL   Вверх
DenWPF
Дата 24.5.2011, 22:27 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1659
Регистрация: 26.9.2009

Репутация: нет
Всего: 1



Не понимаю в чем сложность. берете текст бокс убераете у него стили. строку в цикле по символу вызываете...
PM MAIL   Вверх
AleksPingvin
Дата 25.5.2011, 02:04 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 62
Регистрация: 20.5.2011

Репутация: нет
Всего: нет



Мне не нужен текстбокс.
У меня бегущая строка со сложным фоном, пиктограммами и плавной (а не сдвиг по символу) анимацией.

PM MAIL   Вверх
-Mikle-
Дата 25.5.2011, 06:07 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Невидимка Vingrad'а
***


Профиль
Группа: Экс. модератор
Сообщений: 1672
Регистрация: 22.6.2003
Где: Казахстан, Астана

Репутация: 13
Всего: 59



AleksPingvin, в моей реализации любой контент можно прокручивать, это подходит? Может седня вечером работы меньше будет, напишу.


--------------------
Если тебе плюют в спину, значит ты впереди...
PM   Вверх
AleksPingvin
Дата 25.5.2011, 07:30 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 62
Регистрация: 20.5.2011

Репутация: нет
Всего: нет



-Mikle-, если она исполняет свою главную роль - текст бежит и можно использовать пиктограмки в нем, то да. 
Впрочем все равно опишите идею, лишним не будет.
А заодно пока не подскажете ли как все же для общего развития, как мне программно (а не загрузив из файла) создать изображение на котором я потом смогу программно отрисовать все что захочу smile
PM MAIL   Вверх
DenWPF
Дата 25.5.2011, 15:12 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1659
Регистрация: 26.9.2009

Репутация: нет
Всего: 1



Я конечно не сильно понял, ну а такой вариант не подходит?

если нет, то есть пример того чего ты хочешь?
Код

<StackPanel Width="700" Height="300">
        <Label Name="label1" Content="Бегущая строка" FontSize="32" />
        <Button HorizontalAlignment="Center" Click="Button_Click" Content="Запустить" />
        <Button HorizontalAlignment="Center" Click="Button_Click_1" Content="Остановить" />
    </StackPanel>


Код

private void Button_Click(object sender, RoutedEventArgs e)
        {
            ThicknessAnimation ta = new ThicknessAnimation();
            ta.From = new Thickness(600, 0, 0, 0);
            ta.To = new Thickness(0, 0, 0, 0);
            ta.Duration = TimeSpan.FromMilliseconds(3000);
            ta.RepeatBehavior = RepeatBehavior.Forever;
            label1.BeginAnimation(Label.MarginProperty, ta);
        }
 
        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            label1.BeginAnimation(Label.MarginProperty, null);
        }

PM MAIL   Вверх
AleksPingvin
Дата 25.5.2011, 16:18 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 62
Регистрация: 20.5.2011

Репутация: нет
Всего: нет



Код не запускал, ибо на работе, но на сколько я вижу:
1) Бежит только текст. Я в третий раз говорю - нужно иметь возможность задавать в качестве объекта движения не только текст, но и пикторграммы.
Например: Игра начинается в <пиктограма будильника> 12.00 бла бла бла.
2) Когда строка кончается, то дальше она идет заново не ожидая того, когда ее остаток дойдет до конца. Иными словами если строка "Привет, я дядя Степа!" и вся фраза не влазит  в ширину строки, то будет выглядеть так: "вет, я дядя Степа! При"
PM MAIL   Вверх
DenWPF
Дата 25.5.2011, 17:11 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1659
Регистрация: 26.9.2009

Репутация: нет
Всего: 1



бежит не текст, а Label т.е Контрол, можно любой контрол так.
Сделай обертку с областью видимостью.
ладно, пусть -Mikle- напишет как у него. 
PM MAIL   Вверх
-Mikle-
Дата 25.5.2011, 18:46 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Невидимка Vingrad'а
***


Профиль
Группа: Экс. модератор
Сообщений: 1672
Регистрация: 22.6.2003
Где: Казахстан, Астана

Репутация: 13
Всего: 59



Цитата(DenWPF @  25.5.2011,  20:11 Найти цитируемый пост)
Сделай обертку с областью видимостью.
ладно, пусть -Mikle- напишет как у него.

Ну именно так и сделано у меня. Я решил смотреть в наш контрол и написать новый, но только изрядно урезанный вариант. Тем самым я и весь основной код не выложу (сохраню права на код), но в то же время то что требуется именно топикастеру реализую. В связи с копи-пастами из более навороченного контрола некоторая логика, возможно, будет излишней. Правьте сами, кому не нравится smile.

Контрол делался для силверлайта, но я не предвижу никаких проблем сделать то же самое на WPF.
Итак...

Создаем Templated Control и даем имя CreepLine. Студия нам создает CreepLine.cs и Generic.xaml (в нем будет определяться шаблон контрола). Далее меняем код класса на вот это:
Код

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;

[TemplatePart(Name = PART_CONTAINER, Type = typeof(FrameworkElement))]
[TemplatePart(Name = PART_CONTENT, Type = typeof(FrameworkElement))]
public class CreepLine : ContentControl
{
    private const string PART_CONTAINER = "CONTAINER_PART";
    private const string PART_CONTENT = "CONTENT_PART";

    public const string NormalState = "Normal";
    public const string MarqueeState = "Marquee";

    FrameworkElement container;
    FrameworkElement content;
    string gotoStateName = null;
    DispatcherTimer delayTimer;

    public CreepLine()
    {
        this.DefaultStyleKey = typeof(CreepLine);

        delayTimer = new DispatcherTimer();
        delayTimer.Tick += delayTimer_Tick;
        delayTimer.Interval = TimeSpan.FromMilliseconds(1);
        delayTimer.Stop();
        OnInitialDelayChanged(TimeSpan.Zero, TimeSpan.Zero);

        IsEnabled = false;

        SizeChanged += delegate { ClipToBounds(); };
        Loaded += delegate
        {
            ClipToBounds();
            CalculateProperties(true, true);
        };
        IsEnabledChanged += delegate
        {
            if (IsEnabled) InvalidateState();
            else ImmediatllyGoToState(NormalState);
        };
    }

    protected override Size MeasureOverride(Size availableSize)
    {
        FrameworkElement root = this.Content as FrameworkElement;
        if (root == null) return base.MeasureOverride(availableSize);
        root.Measure(availableSize);

        Size resultSize = availableSize;
        switch (HorizontalAlignment)
        {
            case System.Windows.HorizontalAlignment.Left:
            case System.Windows.HorizontalAlignment.Center:
            case System.Windows.HorizontalAlignment.Right:
                resultSize.Width = !double.IsInfinity(availableSize.Width) ? Math.Min(availableSize.Width, root.DesiredSize.Width) : root.DesiredSize.Width;
                break;
            case System.Windows.HorizontalAlignment.Stretch:
            default:
                resultSize.Width = !double.IsInfinity(availableSize.Width) ? availableSize.Width : root.DesiredSize.Width;
                break;
        }

        switch (VerticalAlignment)
        {
            case System.Windows.VerticalAlignment.Top:
            case System.Windows.VerticalAlignment.Center:
            case System.Windows.VerticalAlignment.Bottom:
                resultSize.Height = !double.IsInfinity(availableSize.Height) ? Math.Min(availableSize.Height, root.DesiredSize.Height) : root.DesiredSize.Height;
                break;
            case System.Windows.VerticalAlignment.Stretch:
            default:
                resultSize.Height = !double.IsInfinity(availableSize.Height) ? availableSize.Height : root.DesiredSize.Height;
                break;
        }

        return resultSize;
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        container = GetTemplatePart<FrameworkElement, Canvas>(PART_CONTAINER);
        content = GetTemplatePart<FrameworkElement, ContentControl>(PART_CONTENT);
        CalculateProperties(true, true);

        content.SizeChanged += delegate
        {
            this.InvalidateMeasure();
            CalculateProperties(false, true);
        };
        container.SizeChanged += delegate
        {
            this.InvalidateMeasure();
            CalculateProperties(true, false);
        };
    }

    private T GetTemplatePart<T, IfNotFoundType>(string templateName)
        where T : class
        where IfNotFoundType : T, new()
    {
        return (this.GetTemplateChild(templateName) as T) ?? new IfNotFoundType();
    }

    private void ClipToBounds()
    {
        Clip = new RectangleGeometry() { Rect = new Rect(0, 0, ActualWidth, ActualHeight) };
    }

    private void CalculateProperties(bool containerChanged, bool contentChanged)
    {
        if (container == null || content == null) return;

        if (containerChanged)
        {
            StartContentAtRight = container.ActualWidth;
        }
        if (contentChanged)
        {
            EndContentAtLeft = -content.ActualWidth;
            this.MinHeight = content.ActualHeight;
        }
        if (containerChanged || contentChanged)
        {
            EndContentAtRight = container.ActualWidth - content.ActualWidth;
            CalculateDuration();
            //makesSenseToAnimate = EndContentAtRight < 0;
            InvalidateState();
        }
    }
    private void CalculateDuration()
    {
        if (container == null || content == null) return;
        Duration = new Duration(
            TimeSpan.FromSeconds(
                (container.ActualWidth * 2 + content.ActualWidth) / PixelsPerSecondSpeed));
    }

    private void delayTimer_Tick(object sender, EventArgs e)
    {
        if (!ImmediatllyGoToState(gotoStateName)) return;

        delayTimer.Stop();
        gotoStateName = null;
    }
    private void InvalidateState()
    {
        GoToState(MarqueeState);
    }
    private void GoToState(string stateName)
    {
        if (!IsEnabled)
        {
            ImmediatllyGoToState(NormalState);
            return;
        }
        if (stateName == NormalState)
            ImmediatllyGoToState(stateName);
        else
            DellayGoToState(stateName);
    }
    private void DellayGoToState(string stateName)
    {
        gotoStateName = stateName;
        delayTimer.Start();
    }
    private bool ImmediatllyGoToState(string stateName)
    {
        try
        {
            CalculateDuration();
            if (string.IsNullOrWhiteSpace(stateName)) stateName = NormalState;
            VisualStateManager.GoToState(this, stateName, true);
            delayTimer.Stop();
            gotoStateName = null;
            return true;
        }
        catch
        {
            return false;
        }
    }


    public static readonly DependencyProperty StartContentAtRightProperty = DependencyProperty.Register("StartContentAtRight", typeof(double), typeof(CreepLine), new PropertyMetadata(0.0));
    public double StartContentAtRight
    {
        get { return (double)GetValue(StartContentAtRightProperty); }
        protected set { SetValue(StartContentAtRightProperty, value); }
    }
    public static readonly DependencyProperty EndContentAtLeftProperty = DependencyProperty.Register("EndContentAtLeft", typeof(double), typeof(CreepLine), new PropertyMetadata(0.0));
    public double EndContentAtLeft
    {
        get { return (double)GetValue(EndContentAtLeftProperty); }
        protected set { SetValue(EndContentAtLeftProperty, value); }
    }
    public static readonly DependencyProperty EndContentAtRightProperty = DependencyProperty.Register("EndContentAtRight", typeof(double), typeof(CreepLine), new PropertyMetadata(0.0));
    public double EndContentAtRight
    {
        get { return (double)GetValue(EndContentAtRightProperty); }
        protected set { SetValue(EndContentAtRightProperty, value); }
    }
    public static readonly DependencyProperty DurationProperty = DependencyProperty.Register("Duration", typeof(Duration), typeof(CreepLine), new PropertyMetadata(Duration.Automatic));
    public Duration Duration
    {
        get { return (Duration)GetValue(DurationProperty); }
        protected set { SetValue(DurationProperty, value); }
    }

    public static readonly DependencyProperty PixelsPerSecondSpeedProperty = DependencyProperty.Register("PixelsPerSecondSpeed", typeof(double), typeof(CreepLine), new PropertyMetadata(75.0, PixelsPerSecondSpeedChanged));
    private static void PixelsPerSecondSpeedChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        ((CreepLine)sender).OnPixelsPerSecondSpeedChanged((double)e.OldValue, (double)e.NewValue);
    }
    public double PixelsPerSecondSpeed
    {
        get { return (double)GetValue(PixelsPerSecondSpeedProperty); }
        set { SetValue(PixelsPerSecondSpeedProperty, value); }
    }
    protected virtual void OnPixelsPerSecondSpeedChanged(double oldValue, double newValue)
    {
        if (newValue < 1)
            PixelsPerSecondSpeed = 1.0;
        else
            CalculateDuration();
    }

    public static readonly DependencyProperty InitialDelayProperty = DependencyProperty.Register("InitialDelay", typeof(TimeSpan), typeof(CreepLine), new PropertyMetadata(TimeSpan.Zero, InitialDelayChanged));
    private static void InitialDelayChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        ((CreepLine)sender).OnInitialDelayChanged((TimeSpan)e.OldValue, (TimeSpan)e.NewValue);
    }
    public TimeSpan InitialDelay
    {
        get { return (TimeSpan)GetValue(InitialDelayProperty); }
        set { SetValue(InitialDelayProperty, value); }
    }
    protected virtual void OnInitialDelayChanged(TimeSpan oldValue, TimeSpan newValue)
    {
        delayTimer.Interval = TimeSpan.FromMilliseconds(Math.Max(1.0, newValue.TotalMilliseconds));
    }
}


Открываем Generic.xaml и находим там стиль для вновь созданного контрола. Редактируем его и приводим в такой вид:
Код

    <Style TargetType="local:CreepLine">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:CreepLine">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">

                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="Animations">
                                <VisualState x:Name="Normal">
                                    <Storyboard Storyboard.TargetName="CONTENT_PART" Storyboard.TargetProperty="(Canvas.Left)">
                                        <DoubleAnimation Duration="00:00:00.3" To="0">
                                            <DoubleAnimation.EasingFunction>
                                                <QuinticEase EasingMode="EaseOut" />
                                            </DoubleAnimation.EasingFunction>
                                        </DoubleAnimation>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="Marquee">
                                    <Storyboard Storyboard.TargetName="CONTENT_PART" Storyboard.TargetProperty="(Canvas.Left)">
                                        <DoubleAnimation From="{Binding StartContentAtRight, RelativeSource={RelativeSource}}"
                                                         To="{Binding EndContentAtLeft, RelativeSource={RelativeSource}}"
                                                         Duration="{Binding Duration, RelativeSource={RelativeSource}}" RepeatBehavior="Forever" />
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>

                        <Canvas x:Name="CONTAINER_PART">
                            <ContentPresenter x:Name="CONTENT_PART"
                                              Content="{TemplateBinding Content}"
                                              ContentTemplate="{TemplateBinding ContentTemplate}"
                                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                              Margin="{TemplateBinding Padding}" />
                        </Canvas>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>


Вот и все. Этот контрол выполняет нужную функцию, а именно, анимирует слева на право любой контент. Свойство IsEnabled говорит о том бежит ли контент или стоит. Свойство InitialDelay задает паузу, перед тем как контент начнет бежать (тут наверное лишнее для этой задачи).

Вот пример использования:
Код

            <local:CreepLine IsEnabled="True" InitialDelay="00:00:05">
                <StackPanel Orientation="Horizontal">
                    <Image Source="Favorites.png" Height="16"/>
                    <TextBlock>Hello world!!! This is the simple example of large text. Offcouse, This is not so large as you are needs, but this is someone.</TextBlock>
                    <Image Source="Favorites.png" Height="16"/>
                </StackPanel>
            </local:CreepLine>


Как видно, можно расположить абсолютно любой контент, с навороченными бэкграундами, фореграундами, форматированием, картинками, да хоть видеоролик сюда вставляй.


--------------------
Если тебе плюют в спину, значит ты впереди...
PM   Вверх
AleksPingvin
Дата 25.5.2011, 19:18 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 62
Регистрация: 20.5.2011

Репутация: нет
Всего: нет



-Mikle-, супер! Попробую утром перекопипастить и потестить ваш компонент) Если возникнут вопросы - обязательно их задам smile
Кстати а "выбег" содержимого начинается с краю или он сразу появляется а только потом начинает двигаться?
PM MAIL   Вверх
-Mikle-
Дата 25.5.2011, 20:55 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Невидимка Vingrad'а
***


Профиль
Группа: Экс. модератор
Сообщений: 1672
Регистрация: 22.6.2003
Где: Казахстан, Астана

Репутация: 13
Всего: 59



Цитата(AleksPingvin @  25.5.2011,  22:18 Найти цитируемый пост)
Кстати а "выбег" содержимого начинается с краю или он сразу появляется а только потом начинает двигаться?

Ну вот завтра и увидишь smile

Не дорассказал еще про свойство PixelsPerSecondSpeed - оно задает скорость перемещения в пикселах (на сколько пикселов двинется контент за одну секунду), по умолчанию стоит 75.

Свойства StartContentAtRight, EndContentAtLeft, EndContentAtRight и Duration - ридонли и служат для поддержки анимации в шаблоне контрола. Ну конечно задать значения этих свойств можно, только не из XAML. Да и незачем стремиться к этому, так как они все равно снова пересчитаются при любом изменении. В WPF можно объявить ридонли депенденси свойства и тогда они действительно будут ридонли, а сильверлайт пока не поддерживает такие.


--------------------
Если тебе плюют в спину, значит ты впереди...
PM   Вверх
AleksPingvin
Дата 26.5.2011, 05:51 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 62
Регистрация: 20.5.2011

Репутация: нет
Всего: нет



-Mikle-, ты меня убьешь... но моя VS 2010 не умеет создавать Template Control-ы... нет у нее такого пунктика( Вот User Control да)... я ее даже обновил и сдк поставил и нифига. 


Это сообщение отредактировал(а) AleksPingvin - 26.5.2011, 07:13
PM MAIL   Вверх
Ответ в темуСоздание новой темы Создание опроса
1 Пользователей читают эту тему (1 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | WPF и Silverlight | Следующая тема »


 




[ Время генерации скрипта: 0.1030 ]   [ Использовано запросов: 21 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.