Мастера DELPHI, Delphi programming community Рейтинг@Mail.ru Титульная страница Поиск, карта сайта Написать письмо 
| Новости |
Новости сайта
Поиск |
Поиск по лучшим сайтам о Delphi
FAQ |
Огромная база часто задаваемых вопросов и, конечно же, ответы к ним ;)
Статьи |
Подборка статей на самые разные темы. Все о DELPHI
Книги |
Новинки книжного рынка
Новости VCL
Обзор свежих компонент со всего мира, по-русски!
|
| Форумы
Здесь вы можете задать свой вопрос и наверняка получите ответ
| ЧАТ |
Место для общения :)
Орешник |
Коллекция курьезных вопросов из форумов
KOL и MCK |
KOL и MCK - Компактные программы на Delphi
 
Лицензия автокад

Создание полноценного чата на Flash

Итак, решил я изучить Flash, нашел книжку по 5-ому флешу просмотрел ее по диагонали, кое что в голове отложилось, но в целом все очень туманно... Поискал в инете учебники по флешу... ничего интересного не нашел, кроме "программирование движения на Flash Action Script" http://dembicki.narod.ru/tutor/index.htm Открыл в первый раз Macromedia Flash MX 2004 потыкался туда-сюда, нарисовал шарик, сделал чтобы он бегал за курсором мыши и понял, придется делать все как обычно: разбираться самостоятельно с нуля.
Во Flash меня в первую очередь интересует программирование, а не рисование мультиков, потому и решил начать свое обучение с написания чата. Посмотрел, что есть в инете на тему "чат на Flash", нашел одну примитивную заметку, где все реализовывалось через refresh с подчитыванием 50-ти последних строк чата и вывода их в виде обычного текста. Попытался найти работающие чаты на Flash - ситуация хуже, чем я предполагал... Если это более-менее нормальный чат, то сам вывод текста реализован в HTML с подгрузкой последних строк через refresh, а на Flash реализованы только строка для ввода текста и смены настроек (http://chat.arkos.ru/)... Если же это чат полностью на Flash, то функционал на уровне детского сада, обычный текст, без смайликов и возможности кликать по ссылкам в тексте или по нику отправителя, чтобы написать ему ответ... За две недели поиска информации по этому вопросу, мне попался только один достойный чат на Flash (http://c.ruse.ru/fc5/): постоянный коннект, удобный интерфейс, продуманный протокол с малым трафиком, стильно оформлен... но нет возможности вставлять смайлики в основном окне общения.
Поэтому, идея создания нормального чата мне еще больше понравилась. А так как я пишу его, не имея навыков работы во Flash, то подробное описание процесса со всеми сделанными ошибками и открытиями будет очень хорошим пособием для начинающих изучать Flash.

1. Постановка задачи

Что же в итоге должно получиться:
  1. Клиентская часть чата должна быть реализована на Flash, серверная на Perl.
    Я хорошо знаю Perl поэтому мне будет проще и быстрее на нем писать, а по большому счету на чем реализовывать серверную часть роли не играет, от этого будет только зависеть сколько народу в online выдержит сервер.
  2. Чат должен держать постоянный коннект с сервером.
    В этом есть свои плюсы и минусы. Плюсы: меньше трафик, больше динамика чата, т.к. нет пауз из-за рефреша, однозначное определение есть человек в online или у него разорвалась связь. Минусы: на сервере необходимо запустить своего демона на выделенном порту (это можно сделать, только имея рутовый доступ), если у юзера на машине стоит FireWall ему надо будет открыть доступ по этому порту, если юзер выходит в инет через прокси, может не получиться соединиться с сервером (не знаю как во Flash решены эти проблемы). И все же, чаты с технологией refresh, на мой взгляд, дело прошлое, и надо идти в ногу со временем, поэтому делаем постоянное соединение.
  3. В чате должны быть смайлики. Ники кликабельны.
  4. Возможность выбрать цвет своих сообщений.
  5. Подсветка сообщений адресованных мне. Возможность установки фильтров.
P.S. Мы не ищем легких задач :)

2. Установка постоянного коннекта с сервером.

Начнем сначала, ввод имени и установка коннекта с сервером. Для обмена информацией с сервером используем объект XMLSocket. Итак, во флеш создаем новый документ и делаем титульную страницу для входа в чат: поле для ввода ника (TextInput используем стандартные компоненты флеша MX из окна components) и кнопку "войти" (Button). Регистрацию делать не будем, это не имеет отношения к самому чату.
Поле ввода ника обзываем login. Кнопку: enter_button.
Кроме этого понадобится еще пустое поле Dynamic Text, куда будем выводить информацию о подключении к серверу и ошибки, если такие возникнут. Поле привязываем к переменной messages. Цвет текста красный, чтобы в глаза бросалось.
В actions кнопки пишем:
on (click) {
    _parent.messages = _parent.login.text;
}
Внешне получилось вот что:

Если нажать Ctrl-Enter, ввести в поле свое имя и нажать на кнопку "Войти", то имя выведется ниже красным цветом. Отлично, уже что-то работает :) Создаем новый слой, обзовем его Scripts, там и будем писать все основные скрипты. Для начала, меняем код нажатия кнопки на:
on (click) {
    if (length(_parent.login.text) < 3) {
        _parent.messages = "Введите ваше имя";
        _parent.login.setFocus();
    } else {
        _root.Connect();
    }
}
Если имя не было введено (или оно меньше трех символов), то ругаемся, иначе переходим к установке соединения с сервером. Саму функцию Connect () пишем в слое Scripts, вот что у меня получилось (большая часть скопирована из хелпа к XMLSocket ;) var serverName = "localhost";
var serverPort = 1024;

// Устанавливает соединение с сервером
function Connect() {
    messages = "Устанавливается соединение с сервером...";
    // Create a new XMLSocket object
    sock = new XMLSocket();
    // Устанавливаем обработчики событий
    sock.onConnect = onSockConnect;    // соединение
    sock.onXML = onGetXML;        // получение данных
    sock.onClose = onSockClose;    // связь утеряна
    // Call its connect() method to establish a connection
    sock.connect(serverName, serverPort);
}

// Define a function to assign to the sock object that handles
// the server's response.
function onSockConnect(success){
if (success){ // соединение установлено
    messages = "Соединение установлено. Вход...";
    doLogin(); // переходим к процедуре передачи нашего логина
} else {
    messages="Не удалось установить соединение с сервером: "+serverName;
}
}

// Передача нашего логина на сервер
function doLogin() {
    var myXML = new XML("<LOGIN NAME=\""+login.text+"\" />");
    sock.send(myXML);
}

// Закрылось соединение с сервером
function onSockClose(){
    gotoAndStop(1); // Перейти на титульную страницу, где бы мы не находились
    messages = "Сервер разорвал соединение.";
}

// Прием данных от сервера
function onGetXML(doc) {
    trace("onXML: "+doc);
    var e = doc.firstChild; // Берем первый элемент
    if (e != null) {
        var s = e.nodeName; // Имя элемента
        if (s == "ERROR") { // Произошла ошибка, разорвать соединение и
            sock.close();
            messages = e.attributes.TEXT; // Вывести сообщение об ошибке
        }
    }
}

Хорошо, теперь нужно немного отвлечься от Flash и написать простенький серверный демон на Perl... куда собственно будем подключаться. Вот код:

#!/usr/bin/perl
use POSIX ();
use Socket;

my $DaemonPort = 1024;
my $work = 1;
$|=1;

my $sock_name = sockaddr_in($DaemonPort, INADDR_ANY)   
        or die "Couldn't convert into an Internet address: $!\n";
socket(SERVER, PF_INET, SOCK_STREAM, getprotobyname('tcp'))
        or die "Couldn't create socket: $!\n";
setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR, 1)
        or die "setsockopt() failed: $!\n";
bind(SERVER, $sock_name)
        or die "Couldn't bind to port $port: $!\n";
listen(SERVER, SOMAXCONN);
$SIG{PIPE} = 'IGNORE';

_log("Server started...");

    my $rem_addr = accept(CLIENT,SERVER);
    next unless (defined $rem_addr);
   
    my($port,$iaddr) = sockaddr_in($rem_addr);
    $IP = inet_ntoa($iaddr);
    _log("Connection from $IP:$port");

    my ($byte, $line);
    while ($work and sysread(CLIENT, $byte, 1) == 1) {
        if (ord($byte) == 0) { goCommand($line) }
        else { $line .= $byte }
}

    sleep(3);    #-- Замрем на 3 секунды
    _log("Die connection.");
    close CLIENT;

close(SERVER);
_log("Server shutdown");
die;

#-- Выводит на экран тестовую информацию
sub _log
{ my ($s) = @_;
    print "".(localtime(time))."\t$s\n";

}

#-- Обработка поступившей от клиента команды
sub goCommand
{ my ($line) = @_;
if (index($line, "<LOGIN")==0) {    #-- Залогинивание
        if ($line=~/NAME=\"([^\"]+)\"/) {
        _log("LOGIN: $1");
#-- Говорим, что такой логин занят (test)
    sendAnswer("<ERROR TEXT=\"Такой логин занят, выберите другой\"/>");   
        }
    }
    $work = 0;
}

#-- Отсылает ответ клиенту. Проблема в том, что русские буквы надо
#-- кодировать в utf и в конце ставим ноль
sub sendAnswer
{ my ($s) = @_;
    print CLIENT utf($s).chr(0);
}

#-- Функции кодирования русских букв нашел где-то в инете, очень не хотелось
#-- цеплять здоровые библиотеки по работе с utf ради такой мелочи
sub utf
{ my $s = shift;
    $s=~s/([А-Яа-яЪЬЁъьё])/win2utf($1)/eg;
    return $s;
}

sub win2utf
{    my $s = shift;
    if ( ord($s)>=192 and ord($s)<=239) { return chr(208).chr(ord($s)-48) }
    if ( ord($s)>=240 and ord($s)<=255) { return chr(209).chr(ord($s)-112)}
    if ($s=="Ё") { return chr(208).chr(149) }
    if ($s=="ё") { return chr(208).chr(181) }
    if ($s=="Ъ") { return chr(208).chr(172) }
    if ($s=="Ь") { return chr(208).chr(170) }
    if ($s=="ъ") { return chr(208).chr(140) }
    if ($s=="ь") { return chr(208).chr(138) }
    return $s;
}
Комментировать тут особо нечего, кто в Perl разбирается, тот поймет, а другим и не надо (мы пишем чат на Flash, а не Perl изучаем :) В двух словах: он ожидает подключения к порту 1024, затем ожидает команду и если это <LOGIN ... то в ответ посылает сообщение об ошибке, что такой логин занят и закрывает соединение.
Здесь вы можете скачать исходники этой версии (chat v1.zip).
Итак, запускаем серверный скрипт, из командной строки:
perl chat_daemon.pl (у вас должен быть установлен Perl на компьютере, скачать его можно на http://www.activestate.com порядка 10Мб.)
И запускаете флешку. Вводим любое имя и нажимаем "Войти", видим сообщение об установке соединения, о том, что соединение установлено и через 3 секунды (такая задержка стоит в серверном скрипте) сообщение об ошибке, что такой логин занят...

3. Протокол.

Итак, с установкой соединения разобрались, теперь нужно подумать о протоколе общения серверной части и клиентской. Здесь обсуждать особенно нечего, как придумаешь, так и реализовывать будешь. Необходимый минимум: залогинивание, получение сообщений, получение списка народа в online, отправка сообщения, вывод сообщений о критических ошибках.
3.1. Залогинивание у нас фактически уже реализовано. Клиент передает на сервер:
<LOGIN NAME="[имя]" /> В ответ получит или сообщение об ошибке, или команда <OK /> - что означает нормальное подключение к чату.
3.2. Сообщение о критических ошибках тоже есть. Сервер передает:
<ERROR TEXT="[текст ошибки]"/>
3.3. Получение списка народа в чате (online). Для экономии трафика, передадим весь список в одном теге, ведь в нашем варианте чата, кроме имени человека ничего больше знать не нужно.
<LIST> [список имен через запятую] </LIST> В целях экономии трафика нужно еще сделать команды: пришел новый человек, ушел человек. Чтобы ради одного имени не отсылать весь список полностью. Это нам дает еще одно преимущество, не формировать сообщения вида "вас приветствует [имя]" и "уходит [имя]" непосредственно на сервере, такие сообщения можно формировать в самом Flash-клиенте. Добавляем еще две команды:
<ADD>[имя]</ADD> - пришел новый человек.
<DEL>[имя]</DEL> - ушел.
3.4. Отправка сообщения на сервер. В целях уменьшения трафика все теги и свойства обозначаем одной буквой. Кроме самого сообщения нужно передавать еще и его цвет.
<T C="[цвет] ">[текст сообщения] </T> C - цвет текста. Если он не указан - пишем черным.
Кому адресовано сообщение и смайлики указываются прямо в тексте. Например в таком виде:
to [кому] - обращение в общем чате к кому-то
private [кому] - отправка приватного сообщения, его увидит только получатель
смайлики в тексте сообщения выделяются двоеточием с двух сторон :smile:, какие именно будут смайлики уточним позже.
3.5. Получение сообщений от серверного скрипта. Делаем аналогично команде отправки сообщения на сервер.
<T C= "[цвет] "> строка текста </T>
C - цвет текста. По умолчанию - черный.

4. Отображение списка болтунов в online.

Переходим непосредственно к чату. Создаем пустой ключевой фрейм. Что и как должно располагаться, см. рисунок.


Займемся отображением списка online, здесь нас сразу подстерегает "биг проблема"
В списке кроме отображения имен нужно отображать еще и кнопочку, для посылки приватных сообщений. Клик по имени - отправка обычного сообщения в общий чат. Клик по картинке приватного сообщения - отправка приватного сообщения, которое видит только адресат. Стандартный компонент List нам не подходит, он позволяет только выбрать один элемент из списка, но не позволяет делать несколько кнопок в одной строчке списка.
Я вижу четыре пути решения этой проблемы. Первые три - это создание нужного списка самостоятельно, последний - подход не программиста, а менеджера.
4.1. Программно формируется слой, где размещаются необходимые кнопки и имена в нужном порядке. Получается большая такая лента, над этим слоем располагается слой маска, с прямоугольной областью в которую мы и наблюдаем видимую часть списка. При этом надо будет сделать кнопочки прокрутки списка вверх - вниз.
Недостаток - если народу в списке будет очень много, то начнутся серьезные тормоза с формированием и прокруткой большой ленты.
4.2. Программно рисуем и выводим имена и кнопочки только видимой области списка. При нажатии кнопок вверх/вниз все элементы сдвигаем вверх/вниз, одну строчку вверху/внизу удаляем, новую вверху/внизу добавляем. Т.е. реализуем прокрутку полностью самостоятельно программно, это устраняет недостаток первого метода. Размеры списка не играют в этом случае никакой роли, но придется больше программировать.
4.3. Делаем список в виде HTML текста и вставляем его в обычный TextField. Здесь обязательно использование Flash 7.0, т.к. только он умеет вставлять картинки в HTML тексте, или же отказаться от картинки и сделать кнопку привата текстом.
4.4. Изменить интерфейс. Например, сделать только список имен, при этом: один клик по имени - обычное сообщение, второй клик - приватное.

Реализуем последний способ, т.к. один из первых двух частично нам придется делать при отображении самого окна чата.
Существенно переделывается серверный скрипт, добавляем поддержку нескольких коннектов, проверку имен, и т.п. Здесь не привожу код, кому интересно посмотрит его в архиве.
Во Flash, вставляем проверку на вводимое имя. Какие должны быть ограничения:

Вот такая функция у меня получилась:
// Проверка правильности ввода имени
function CheckLogin(login) {
    var len = length(login);
    if (len <= 0) { return "Введите ваше имя" }
    else if (len < 3 or len > 12) { return "Длина имени должна быть от трех символов до 12-ти" }
    else if (login.charAt(0) == " " or login.charAt(len-1) == " ") {
        return "Имя не может начинаться и заканчиваться пробелом";
    } else if (login.indexOf(" ")>=0) {
        return "Имя не может содержать два пробела подряд";
    }
    var i = login.indexOf(":");
    if (i >= 0 and login.indexOf(":", i+1)>0) {
        return "В имени нельзя использовать два двоеточия";
    }
    var stop_chars = Array("<", ">", "'", "[", "]", ",", "\"");
    for (i in stop_chars) {
        if (login.indexOf(stop_chars[i])>=0) return "В имени использован запрещенный символ: "+stop_chars[i];
    }
    // составим массив русских и английских букв
    var rus_chars = "абвгдежзийклмнопрстуфхцчшщьыъэюяАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯЁё".split("");
    var eng_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
    // подсчитаем количество букв обоих алфавитов
    var rus_count = 0, eng_count = 0;
    for (i in rus_chars) { if(login.indexOf(rus_chars[i])>=0) rus_count++; }
    for (i in eng_chars) { if(login.indexOf(eng_chars[i])>=0) eng_count++; }
    if (rus_count>0 and eng_count>0) return "В имени разрешено использовать только буквы одного алфавита русского или английского. Нельзя смешивать.";
    if (rus_count<2 and eng_count<2) return "В имени обязательно должны содержаться хотя бы две буквы";
    return "";
}
Добавим в место "список народа online" компонент List и назовем его listonline.
Далее необходимо переделать функцию onGetXML. Во-первых, возможно поступление сразу нескольких команд от сервера. Во-вторых, добавим обработку тегов OK и LIST.
Вот что получилось:
// Прием данных от сервера
function onGetXML(doc) {
    var e = doc.firstChild;
    while (e != null) {    // Обход всего XML дерева
        var s = e.nodeName;
        if (s == "ERROR") { // Произошла ошибка, разорвать соединение и вывести текст ошибки
            sock.close();
            messages = e.attributes.TEXT; // Вывести сообщение об ошибке
        } else if (s == "OK") { // Мы вошли в чат
            online = true;
            nextFrame();    // Переход на второй кадр, где и расположен интерфейс чата
        } else if (online) { // Все остальные команды обрабатываются только, когда мы в online
            if (s == "LIST") { // Пришел список народа
                // Преобразуем в массив
                var logins = e.firstChild.nodeValue.split(",");
                listonline.removeAll();    // Очистим list
                for (var i in logins) {    // Добавляем все логины
                    listonline.addItem(logins[i]);
                }
                listonline.sortItemsBy("label", "ASC"); // Сортируем по возрастанию
            }
        }
        e = e.nextSibling;    // переход к следующему элементу XML дерева
    }
}
С этим кодом мучался несколько часов. Не появляется список народа во флешке, хоть ты тресни! Приходит команда от сервера (<OK /><LIST>atest1,test2,Петя,Random - 99</LIST>), нормально обрабатывается флешкой, но список остается девственно чист. Причина, как оказалось, кроется в том, что переход на второй фрейм (кадр) происходит не сразу, как встретилась команда nextFrame() , а только по завершению выполнения функции onGetXML, чем это нам мешает? А тем, что компонента listonline на момент выполнения этой функции еще не существует, т.к. реально перехода на второй фрейм еще не было!
Как можно решить эту проблему?
Сделаем так. Функцию doLogin() удалить. Функция onSockConnect() выглядит теперь так:
function onSockConnect(success){
if (success){ // соединение установлено, переходим к процедуре передачи нашего логина
        _global.mylogin = login.text;    // Запоминаем введенный логин
        nextFrame();
} else {
        messages="Не удалось установить соединение с сервером: "+serverName;
}
}
А во втором слое, на входе (Layer 1: Frame 2) пишем:
var myXML = new XML("<LOGIN NAME=\""+_global.mylogin+"\" />");
sock.send(myXML);
stop();
Т.е. перед тем, как передать наш логин на сервер, запоминаем его в глобальной переменной (_global.mylogin) и переключаемся на второй фрейм(кадр). И вот только когда он полностью загрузится и выполнится код реально передающий наш логин на сервер, и по приходу оттуда ответа у нас уже все готово, для отображения списка народа online.

Пока изучал help по ActionScript нашел полезную вещь. Если на старте первого кадра (Layer 1: Frame 1) написать:
focusManager.defaultPushButton = enter_button;
то при вводе в поле имени, нажатие кнопки Enter равносильно нажатию мышкой на кнопку "вход".

Дефолтная функция
listonline.sortItemsBy("label", "ASC"); сортирует список с учетом регистра, что не очень хорошо, поэтому заменяем ее на свой вариант:
listonline.sortItems(upperCaseFunc);
. . .
// Сортировка без учета регистра
function upperCaseFunc(a,b){
return a.label.toUpperCase() > b.label.toUpperCase();
}
Теперь можно добавить поле для ввода текста, используем уже знакомый компонент TextInput, обзовем его entertext.
В actions списка имен (listonline) пишем функцию добавления текста в строку ввода при выборе имени человека в списке:
on (change) {
    _root.AddName(selectedItem.label);
}
В слой Scripts добавляем функцию добавления имени:
// Добавляет в строку ввода to [имя] или private [имя]
function AddName(s) {
    var i;
    if ((i=entertext.text.indexOf("to ["+s+"]"))>=0) {    // строка уже содержит обращение to [имя]
        // меняем на private [имя]
        entertext.text = entertext.text.substr(0, i)+"private"+entertext.text.substr(i+2);
    } else
    if ((i=entertext.text.indexOf("private ["+s+"]"))>=0) {    // строка уже содержит обращение private [имя]
        // меняем на to [имя]
        entertext.text = entertext.text.substr(0, i)+"to"+entertext.text.substr(i+7);
    } else {    // строка не содержит обращения по этому имени
        // добавляем to [имя]
        entertext.text = "to ["+s+"] "+entertext.text;
    }
}
Отлично. Теперь сделаем отправку введенного текста на сервер. Добавим справа от поля ввода кнопочку (компонент Button) с именем ok_button. Сделаем чтобы она была дефолтной, т.е. при нажатии кнопки Enter в поле ввода срабатывало on(click) этой кнопки. Для этого при открытии второго фрейма(кадра) (Layer 1: Frame 2) добавим строчку:
focusManager.defaultPushButton = ok_button; И теперь в actions кнопки пишем:
on (click) {
    _root.SendTextToServer(_parent.entertext.text);// отправка введенного текста на сервер
    _parent.entertext.text = "";    // очистка строки ввода
    _parent.entertext.setFocus();    // поставим фокус на строку ввода
}
И в слой Scripts добавляем функцию:
// Отправка введенного текста на сервер
function SendTextToServer(text :String) {
    if (length(text)==0) return ;    // выход, если нет текста
    var myXML = new XML();            // новый XML
    var myNode = myXML.createElement("T");    // создаем тег "T"
    // добавляем текст в тег
    myNode.appendChild(myXML.createTextNode("["+_global.mylogin+"] "+text));
    // добавляем созданный тег в основной XML документ
    myXML.appendChild(myNode);
    // отправляем на сервер
    sock.send(myXML);
}
Создавать XML надо именно так, а не: var myXML = new XML("<T C="+color+">"+text+"></T>"); т.к. в строке text могут содержаться кавычки и угловые скобки, которые надо самостоятельно заменить на: &quot; &gt; &lt; а функция createTextNode делает это автоматически.

Теперь сделаем простой прием сообщений от сервера (обработка тегов <T>), чтобы проверить работоспособность всего вышеописанного.
Добавим "Text tool" тип "Dinamic text" на место "Основное окно с текстом чата". Шрифт _sans, размер 16, selectable, Render text as HTML, show border, имя "output_txt".
В функцию onGetXML добавляем простую обработку тега <T>:
if (s == "T") { // Пришел текст в чат
    var color = e.attributes.C;
    var txt = e.firstChild.nodeValue;
    output_txt.htmlText += txt+"<BR>"; // добавляем текст и перевод строки
    output_txt.scroll=output_txt.maxscroll;    // прокручиваем в конец списка
}
Выводим только текст без обработки на цвет, кнопки, ссылки и смайлики.
Здесь же добавим обработку тегов <ADD> и <DEL> они совсем простые.
if (s == "ADD") { // Добавить нового человека в список online
    var new_login = e.firstChild.nodeValue;
    listonline.addItem(new_login);
    listonline.sortItems(upperCaseFunc);    // снова отсортировать список
    AppendText("входит: "+new_login);    // добавляем текст
} else if (s == "DEL") { // Удалить человека из списак online
    var del_login = e.firstChild.nodeValue;
    // Ищем такое имя в нашем списке
    for (var i=0; i<listonline.length; i++) {
        if (listonline.getItemAt(i).label == del_login) { // нашли
            listonline.removeItemAt(i);
            AppendText("уходит: "+del_login); // добавляем текст
            break; // выходим из цикла
        }
    }
}
Можно подвести промежуточные итоги. Что получилось на данный момент? Есть нормальное подключение к серверу с обработкой ошибок и проверкой синтаксиса имени юзера. Реализовано получение списка народа online от сервера, изменения в списке и соответствующий интерфейс к списку. Отправка сообщений на сервер и получение сообщений от сервера с простым выводом. Т.е. уже сформировался рабочий каркас для хорошего чата.
Здесь вы можете скачать архив с текущей версией чата (chat v2.zip).
Необходимо запустить серверный демон:
perl chat_daemon.pl и затем можно открыть несколько чатов (chat.swf) и поговорить самому с собой от имени нескольких человек.

5. Выбор цвета

Сделаем выбор цвета для текста в чате. Можно создать несколько цветных квадратиков и по щелчку по ним выбирать цвет, можно сделать выпадающий список с названиями стандартных цветов, но это все не наш путь, слишком просто ;)
Сделаем цветную панель а-ля Photoshop. Т.к. функция по созданию такого объекта будет довольно большой, то сделаем отдельный слой (назовем его ColorPicker) только под него.
Сама функция создания такого объекта выглядит так:
// Создает поле выбора цвета
// name - имя нового объекта
// x1,y1 - координаты, где он должен размещаться
// width, height - размеры
// depth - глубина объекта
// funcSetNewColor - ссылка на функцию, которая вызывается при смене цвета,
// передает один параметр: цвет
function CreateColorPicker(name, x1, y1, width, height, depth, funcSetNewColor) {
    _root.createEmptyMovieClip(name, depth);    // создание мувикла
    with (_root[name]) {
        // создание горизонтального градиента, базовые 7 сегментов
        var colors = [0xFF0000, 0xFFFF00, 0x00FF00, 0x00FFFF, 0x0000FF, 0xFF00FF, 0xFF0000];
        var alphas = [100, 100, 100, 100, 100, 100, 100];
        var ratios = [];
        for (var i = 0; i<7; i++) { ratios[ratios.length] = (255/6)*i }
        var matrix = {matrixType:"box", x:0, y:0, w:width, h:height, r:0};
        beginGradientFill("linear", colors, alphas, ratios, matrix);
        moveto(0, 0);
        lineto(0, height);
        lineto(width, height);
        lineto(width, 0);
        lineto(0, 0);
        endFill();

        // создание осветления/затемнения по вертикали
        colors = [0xFFFFFF, 0xFFFFFF, 0, 0];
        alphas = [100, 0, 0, 100];
        ratios = [0, 255/4, 255/2, 255];
        matrix = {matrixType:"box", x:0, y:0, w:width, h:height, r:0.5*Math.PI};
        beginGradientFill("linear", colors, alphas, ratios, matrix);
        moveto(0, 0);
        lineto(0, height);
        lineto(width, height);
        lineto(width, 0);
        lineto(0, 0);
        endFill();

        // параметры объекта
        _x=x1; _y=y1;
        _width=width;
        _height=height;
        this.onSetNewColor = funcSetNewColor;
    }
    // обработка событий
    _root[name].onRollOver=function(){    // мышь над объектом
        // создание курсора из библиотеки
        _root.attachMovie("cur","cur",30000,{_x:_root._xmouse,_y:_root._ymouse})
        Mouse.hide();    // скрываем системную мышку
        _root.cur.startDrag();    // начинаем таскаться за мышкой
    }
    _root[name].onRollOut = _root[name].onReleaseOutside = function(){    // мыш ушла с объекта
        _root.cur.stopDrag();    // перестаем "таскаться"
        _root.cur.removeMovieClip();    // удаляем курсор
        Mouse.show();    // показываем системную мышку
    }
    _root[name].onPress=function(){    // щелчок мышкой
        if(hitTest(_root._xmouse,_root._ymouse,true)){    // обрабатываем только если щелкнули по нам
            // вычисляем цвет и вызываем нужную функцию
            onSetNewColor(check_color(this._xmouse,this._ymouse, this._width, this._height));
        }
    }
}
// функция вычисляет цвет по координате мыши, ширине и высоте объекта
function check_color(ux, uy, width, height) {
    var perx = width/6;
    var cperx = 255/width;
    var r=0,g=0,b=0;
    switch (Math.floor(ux/perx)) {
    case 0 :
        r = 255;
        g = (cperx*ux);
        b = 0;
        break;
    case 1 :
        r = cperx*(2*perx-ux);
        g = 255;
        b = 0;
        break;
    case 2 :
        r = 0;
        g = 255;
        b = cperx*(ux-2*perx);
        break;
    case 3 :
        r = 0;
        g = cperx*(4*perx-ux);
        b = 255;
        break;
    case 4 :
        r = cperx*(ux-4*perx);
        g = 0;
        b = 255;
        break;
    case 5 :
        r = 255;
        g = 0;
        b = cperx*(width-ux);
        break;
    case 6 :
        r = 255;
        g = 0;
        b = 0;
        break;
    }
    var h2 = height/2;
    if (uy>h2) {
        var hp = (height-uy)/h2;
        r *= hp;
        g *= hp;
        b *= hp;
    } else if (uy<h2) {
        var temp = (h2-uy)/h2;
        r += temp*(255-r);
        g += temp*(255-g);
        b += temp*(255-b);
    }
    return (r << 16 | g << 8 | b);
}
Здесь все очень просто, хоть и выглядит объемно... Сначала создается пустой мувиклип, затем он заполняется двумя градиентами, картинка получается почти как в Photosop.

Далее описываются функции для трех событий:
Интересный глюк! Если в Document publish settings стоит version: Flash Player 7, то не работает функция beginGradientFill , вместо градиентной заливки видите чистый лист! Не работают даже примеры из Help... как только меняем настройки на Flash Player 6 - все отлично работает. Так что в нашем случае пришлось поставить publish settings в Flash Player 6, благо мы пока ничего из седьмого не использовали...

При входе во второй кадр добавляем функцию создания объекта выбора цвета и функцию установки выбранного цвета.
_root.CreateColorPicker("picker", 580, 370, 40, 26, 10, setNewColor);
. . .

// Функция установки нового цвета
function setNewColor(color) {
    _global.myColor = color;    // запомним цвет в глобальной переменной
    _root.entertext.setStyle("color", color); // поменяем цвет текста в поле ввода
}
В таком варианте будет проблема, если юзер выбирает цвет близкий к цвету фона основного окна чата, тогда его текст будет сливаться с фоном, нужно вставить проверку.
Получим цвет фона поля ввода:
var testcolor = entertext.getStyle("backgroundColor"); Теперь проверка, переведем цвета в RGB и сравним по каждому каналу, если разница меньше порогового значения, то затемним новый цвет. Вот что у меня получилось:
// Функция установки нового цвета
function setNewColor(newcolor) {
    var min_dif = 30;    // минимальная разница между фоном и цветом текста
    var testcolor = entertext.getStyle("backgroundColor");// фоновый цвет поля ввода
    var c1 = extractRGB(testcolor);    // преобразуем фоновый цвет в RGB
    var c2 = extractRGB(newcolor);    /// преобразуем новый цвет в RGB
    // сравниваем
    while (Math.abs(c1.r-c2.r)<min_dif and
        Math.abs(c1.g-c2.g)<min_dif and
        Math.abs(c1.b-c2.b)<min_dif)
    {
        c2.r-=3; c2.g-=3; c2.b-=3; // затемняем
        if (c2.r<0) c2.r=0;
        if (c2.g<0) c2.g=0;
        if (c2.b<0) c2.b=0;
    }
    newcolor = extractColor(c2);
    _global.myColor = newcolor.toString(16);// запомним цвет в глобальной переменной
    entertext.setStyle("color", newcolor);//поменяем цвет текста в поле ввода
}
// Возвращает RGB составляющую цвета
function extractRGB(clr) {
    var B = clr & 0xFF; clr = clr >> 8;
    var G = clr & 0xFF; clr = clr >> 8;
    var R = clr & 0xFF;
    return {r:R,g:G,b:B};
}
// Из RGB возвращает цвет
function extractColor(rgb) {
    return ((rgb.r & 0xFF) << 16 | (rgb.g & 0xFF) << 8 | (rgb.b & 0xFF));
}
Далеко не лучший вариант, но для нашего случая вполне подойдет. Теперь при попытке установить цвет близкий к белому (вернее, близкий к фоновому строки ввода) он немного затемняется, так чтобы быть всегда заметным на фоне.
Осталось передать этот цвет на сервер при отправке текста. В функцию SendTextToServer добавим строчку:
// добавляем свойство "цвет", если он указан
if (_global.myColor != undefined) {myNode.attributes.C = _global.myColor}

6. Настройки.

Что хотелось бы иметь в чате из настроек, кроме выбора цвета текста? Перейдем к созданию кнопок настройки. Сначала фильтр: три фиксированных состояния, переключающиеся по кругу.
В библиотеке делаем графический объект Create new Symbol (Graphic) назовем SystemButton, и рисуем шаблон системных кнопок. Что-то вроде этого:
Далее в библиотеке создаем еще один объект, на этот раз Movie clip, назовем его Filter_button. Два слоя, на нижнем SystemButton (фоновое изображение кнопки), а на верхнем схематичное изображение состояния переключателя. Я это изобразил в виде трех строчек текста (в следующем состоянии их становится две и далее одна). В Action пишем stop(); Чтобы клип остановился на этом кадре, а не стал нам прокручивать все состояния.
Создаем второй кадр (ключевой), где в верхнем слое убираем одну строку текста, так же пишем stop(); Далее совершенно аналогично создаем еще один кадр, где оставляем одну строчку текста.
Готово. Теперь добавляем созданный клип кнопки в наш чат. Сделаем переключение режимов, в Actions этой кнопки пишем:
on(release) {    // переключение режима фильтрации
    nextFrame();
    if (_root.Filter++ >= 3) _root.Filter=1;
}
И там же, где объявлены все глобальные переменные (Scripts - Frame 1) объявим и переменную для фильтра:
var Filter = 1;    // Режимы фильтра: 1-показываем все,
            // 2-удаляем системные сообщения,
            // 3-оставляем только личную переписку
Посмотрим, что получилось. Кнопка работает, переключается, но из последнего состояния не переходит в начало... что, собственно, логично :)
Вернемся в редактирование клипа кнопки, надо добавить четвертый кадр, менять на нем ничего не надо, а в его Actions написать: gotoAndStop(1); Что заставит переключиться кнопку из последнего состояния в первое.
Надо добавить звук. Звуков у нас будет всего два: нажатие кнопок (щелчок), и получение сообщения в чате адресованное нам. В интернете много разных wav файлов, выбрать подходящие не проблема. Добавим их в библиотеку, в свойствах надо выставить параметры сжатия, для получения преемлемого звучания и минимального объема. И назначить им имена в Linkage (без этого нельзя их подгрузить программно), пусть будет btn и message. Теперь на входе во второй кадр (раньше они нам не понадобятся) создадим переменные для управления звуком:
// подключаем звуки
btnSound = new Sound();
btnSound.attachSound("btn")
mesSound = new Sound();
mesSound.attachSound("message");
И событие нажатия на кнопку переключения фильтра теперь будет выглядеть так:
on(release) {    // переключение режима фильтрации
    nextFrame();
    _root.btnSound.start();
    if (_root.Filter++ >= 3) _root.Filter=1;
}
Отлично, вот только человеку, который впервые попал в наш чат, значение этой кнопки будет непонятно, необходимо сделать подсказки, и чем больше - тем лучше.
Выводить подсказки будем программно по мере надобности. Вот такую функцию для отображения Hints (подсказок) я нашел на www.flasher.ru (немного изменил и упростил):
// Hint (всплывающая подсказка)
function alt(altTxt) {
    if (_root["altfield"]!=undefined) {//hint уже создан,просто меняем текст
        _root.altfield.text = altTxt;
        return ;
    }
    // создаем hint
    _root.createTextField("altfield", 778, 0,0,200, 20);
    with (_root.altfield) {    // выставляем параметры
        text = altTxt;
        autoSize = "left";   
        background = 1; border = 1;
        backgroundColor = 0xFFFFE1; selectable = 0;
        _y = _root._ymouse - _height;
        _x = _root._xmouse - _width+10;
    }   
}
// удаляем hint
function removeAlt() {
    _root.altfield.removeTextField();
}
Добавляем это в слой Scripts. А в Actions нашей кнопки пишем:
on (rollOver) {    // вывод подсказки
    _root.alt("Фильтрация сообщений в чате\n- "+
    (_root.Filter==1?"показывать все":
    _root.Filter==2?"не отображать системные сообщения":
    "только моя переписка"));
}
on (rollOut, ReleaseOutside) {    // удаляем подсказку
    _root.removeAlt();
}
Готово. Теперь при наведении мыши на кнопку всплывает подсказка, которая сообщает о назначении этой кнопки и ее текущем состоянии.

Когда мышь уходит с кнопки (rollOut), подсказка удаляется. Необходимо удалять подсказку и по событию (ReleaseOutside), это происходит когда нажать кнопку мыши, оттащить мышку в сторону и за объектом отпустить. При этом мышка ушла с объекта, а события rollOut не произошло, и подсказка не исчезает.

Теперь сделаем кнопку вкл./выкл. звука. Создаем в библиотеке новый Movie Clip, все аналогично предыдущей кнопке, только здесь всего два состояния. События на этой кнопке будут следующие:
on(release) {    // вкл./выкл. звука
    nextFrame();
    if (_root.btnSound.getVolume()==0) {    // включить звуки
        _root.btnSound.setVolume(100);
        _root.mesSound.setVolume(100);
    } else { // выключить звуки
        _root.btnSound.setVolume(0);
        _root.mesSound.setVolume(0);
    }
    _root.btnSound.start();
}
on (rollOver) {    // вывод подсказки
    _root.alt("Вкл./выкл. звука");
}
on (rollOut, releaseOutside) {    // удаляем подсказку
    _root.removeAlt();
}
Наверное, есть возможность выключить все звуки во флешке более простым способом, но лень разбираться, когда и так работает :)

Пора заняться смайликами. Сначала делаем кнопочку, по нажатию на которую будет открываться окно со смайликами. Кнопочку делаем аналогично предыдущим, но у нее все гораздо проще, только одно состояние (т.е. один кадр).
В библиотеке создаем несколько Movie clip и рисуем смайлики, на сколько терпения хватит. Смайлики обзываем smile0, smile1, smile2 и т.д. обязательно ставим linkage с таким же именем, чтобы можно было их создавать программно!
Далее, создаем еще один Movie clip (сразу в свойстве Linkage ставим имя SmileSelect), где рисуем поле, на котором будут размещаться смайлики. Например, так:

Показывать и убирать это окно будем программно. Сначала его нужно создать, вместе с набором смайликов. Создадим глобальный массив (в Scripts - Frame 1), где впишем, как смайлики отображаются в тексте:
var SmileName = // имена смайликов. smile0 == :) , smile1 == :rotate: и т.д.
    [":)", ":rotate:", ":yes:", ":no:", ":P"];
На входе во второй кадр (Layer 1- Frame 2), пишем:
// Создаем окно выбора смайликов
_root.attachMovie("SmileSelect", "ss_mc", 20);
with (_root.ss_mc) {
    _x = 520 - _width;
    _y = 395 - _height;
    _visible = false;
}
// заполняем смайлами
var x=13, y=12, dy=23, dh=21;
for (var i=0; i<SmileName.length; i++) {
    _root.ss_mc.attachMovie("smile"+i, "smile"+i+"_mc", dh++);
    var j = eval("_root.ss_mc.smile"+i+"_mc");
    if (x+j._width > _root.ss_mc._width - 10) { x=13; y+=dy; dy=23; }
    j._x=x; j._y=y;
    j.SmileText = SmileName[i];
    x+=j._width+7;
    if (j._height+7 > dy) { dy = j._height+7 }

    j.onPress = function() { _root.AddSmile(this.SmileText); }
}
Создаем объект с именем ss_mc, координаты где-то в правом нижнем углу (так чтобы своим правым краем залезал на кнопку выбора смайликов) и делаем невидимым
_visible = false; Далее заполняем окно смайликами, в цикле по массиву SmileName, где перечислены все смайлы, создаем новый MovieClip:
_root.ss_mc.attachMovie("smile"+i, "smile"+i+"_mc", dh++); Имя в библиотеке "smile"+i (smile0, smile1, и т.д.), новое имя "smile"+i+"_mc" (на самом деле, оно нам совсем не нужно, но раз надо...), глубина в стеке. Дальше задаются координаты x и y для этого клипа со смайлом, запоминаем его имя во внутренней переменной SmileText и создаем обработчик нажатия на него мышкой onPress, который вызывает глобальную функцию AddSmile, передавая ей имя смайла.
Здесь есть маленький недочет, при создании каждого смайлика создается новый обработчик onPress, хотя внутри он у всех одинаковый. При этом впустую расходуется память. В данном случае это пустяки, но лучше привыкать писать все правильно. Надо сделать один обработчик onPress следующим образом:
j.onPress = myOnPress;
. . .
function myOnPress() { _root.AddSmile(this.SmileText); }
Функция AddSmile выглядит так:
// Добавляет в строку ввода смайлик
function AddSmile(smile) {
    with(_root.entertext) {
        // удаляем пробелы в конце строки
        var i = text.length-1;
        while (i >= 0 and text.charAt(i)==" ") { i-- }
        if (i<=0) { text = smile+" " }
        else { text = text.substring(0, i+1)+ " "+smile+" " }
    }
    // TODO: еще надо бы передвинуть текстовый курсор в конец строки. Пока не ясно как.
}
Далее указывается, когда окно будет исчезать.
// Закрытие окна со смайликами, когда мышка уходит из окна
this.onEnterFrame = function() {
    if (_root.ss_mc._visible and !_root.ss_mc.hitTest(_root._xmouse,_root._ymouse,false)) {
        _root.ss_mc._visible = false;
    }
}
По событию onEnterFrame проверяем, если окно со смайлами сейчас видимо, а курсор мыши не над ним, то убрать окно (сделать невидимым). Т.е. как только мышка уйдет с окна - оно тут же исчезает.
Осталось указать, а когда это окно должно появляться? В actions кнопки выбора смайликов пишем:
on(release) {    // выбор смайликов
    _root.btnSound.start();
    _root.ss_mc._visible = true;
}
Бибикнуть и отобразить окно.
Вроде бы с настройками все.
Что у нас получилось на данный момент, вы можете посмотреть здесь: chat v3.zip

7. Основное окно чата

Пора всерьез заняться окном чата, оно должно нам показывать текст сообщений разными цветами, свой цвет у времени сообщения, свой цвет у системных сообщений (пришел/ушел [имя]), графические смайлики, кликабельные имена и ссылки.
Cначала подсветим время сообщения, для этого я вижу два пути, вставить теги <font color="..."> или же использовать новую фишку Flash 7 таблицы стилей CSS. Так как сам текст сообщения нам тоже придется выводить указанным цветом (а он пусть будет произвольный, а не дискретный), а это осуществляется только тегом font + color, то CSS трогать не будем.
Когда приходит строка текста от сервера у нас вызывается функция AppendText(txt, color), которая и добавляет новую строку текста в окно чата. Вот созданием этой функции сейчас и займемся. Для начала определим цвета (где все глобальные переменные Scripts - Frame1):
var chatColors = {                // цвета в чате
        date:"00A000",         // дата сообщения
        system:"A00000",        // системные сообщения
        name:"0000CC"            // подсветка имени
    };
Все сообщения в чате построены по одному шаблону:
Дата/системная_фраза пробел [в скобках имя] пробел произвольный текст Попробуем выделить цветом первое слово. Находим первый пробел и подсвечиваем в зависимости от того: первый символ цифра или буква.
// Добавляет в поле отображения чата новый текст
function AppendText(txt, color) {
    var i=txt.indexOf(' ');
    if (i>0) {
        var clr = "000000";    // цвет первого слова
        if (txt.charCodeAt(0) < 58) {    // первая цифра. это дата
            clr = chatColors.date;
        } else { // первая буква. это системное сообщение
            clr = chatColors.system;
        }
        // Выделяем цветом clr первое слово/дату
        txt = "<font color=\"#"+clr+"\">" + txt.substr(0,i) +
            "</font>" + txt.substr(i);
    }
    output_txt.htmlText += txt+"<BR>";// добавляем текст и перевод строки
    output_txt.scroll=output_txt.maxscroll;    // прокручиваем в конец списка
}
Далее надо проверить, если после пробела стоит [имя], то его надо сделать гиперссылкой, по нажатию на которую это имя добавляется в строку ввода текста. Делается это при помощи asfunction. Вот пример из хелпа:
function MyFunc(arg){
    trace ("You clicked me! Argument was "+arg);
}
myTextField.htmlText ="<A HREF=\"asfunction:MyFunc,Foo \">Click Me!</A>";

When the hyperlink is clicked, the following results are displayed in the Output panel:

You clicked me! Parameter was Foo
Вроде бы все понятно. Добавляем в нашу функцию проверку наличия имени и правим строчку, добавляя эту asfunction:
var i=txt.indexOf(' ');
if (i>0) {
    var clr = "000000";    // цвет первого слова
    if (txt.charCodeAt(0) < 58) {    // первая цифра. это дата
        clr = chatColors.date;
    } else { // первая буква. это системное сообщение
        clr = chatColors.system;
    }
    var j;
    if (txt.charAt(i+1)=='[' and (j=txt.indexOf(']'))>i){ //есть имя,выделяем
        txt = txt.substr(0,i)+" <a href=\"asfunction:AddName," +
            txt.substring(i+2,j)+"\">"+
            "<font color=\"#"+chatColors.name+"\">"+
            txt.substring(i+1,j+1)+"</font></a>"+
            txt.substr(j+1);
    }
    // Выделяем цветом clr первое слово/дату
    txt = "<font color=\"#"+clr+"\">"+txt.substr(0,i)+
        "</font>"+txt.substr(i);
}
Пояснять вроде бы нечего, чистая работа со строками (тяжело вздыхая по любимому Perl-у, там вся эта функция записывается в две строки). Единственное, нам пришлось добавить выделение ссылки цветом, т.к. Flash сам ссылки не подсвечивает. Обратите внимание, перед тегом "<a " мы заменили пробел на &nbsp; т.к. обычный пробел (и два и три) перед ссылкой Flash почему-то удаляет...
Теперь можно выделить цветом весь остальной текст. Если цвет не указан, заменяем его на черный.
if (color == undefined) { color="000000" }
// Выделяем цветом clr первое слово/дату
txt = "<font color=\"#"+clr+"\">" + txt.substr(0,i) + "</font>" +
    "<font color=\"#"+color+"\">" + txt.substr(i) + "</font>";
Вот уже наш чат становится красивым разноцветным.
Далее, для удобства общения желательно выделять сообщения, которые писал я или которые адресованы мне, сделаем это выделением даты жирным шрифтом. Например, так:
var i=txt.indexOf(' ');
if (i>0) {
    var j, FromName;
    if (txt.charAt(i+1)=='[' and (j=txt.indexOf(']'))>i){ //есть имя,выделяем
        FromName=txt.substring(i+2,j); //запомним,от кого пришло сообщение
        txt = txt.substr(0,i)+" <a href=\"asfunction:AddName,"+
            FromName+"\">"+"<font color=\"#"+chatColors.name+
            "\">"+txt.substring(i+1,j+1)+"</font></a>"+
            txt.substr(j+1);
    }
    var clr = "000000";    // цвет первого слова
    // выделить строку?
    var from_me = (FromName == _global.mylogin); // эту строчку писал я
    var to_me = ( txt.indexOf(" to ["+_global.mylogin+"] ")>0 or //ко мне обращаются
        txt.indexOf(" private ["+_global.mylogin+"] ")>0 ); //мне приватно
    var sel = to_me or from_me;
    if (Filter == 3 and !sel) return ; // фильтрация
    if (to_me) _root.mesSound.start(); // звук по приходу сообщения для меня
    if (txt.charCodeAt(0) < 58) {    // первая цифра. это дата
        clr = chatColors.date;
    } else { // первая буква. это системное сообщение
        clr = chatColors.system;
    }
    if (color == undefined) { color="000000" }
    // Выделяем цветом clr первое слово/дату
    txt = "<font color=\"#"+clr+"\">" + (sel?"<b>":"") +
        txt.substr(0,i) + (sel?"</b>":"") + "</font>" +
        "<font color=\"#"+color+"\">" + txt.substr(i) + "</font>";
}
По хорошему, надо бы выделить фон даты или системного сообщения, а не менять жирность, но я так и не нашел возможности смены фонового цвета, поэтому используем жирность. Обратите внимание, мы здесь добавили фильтрацию, если Filter == 3 оставлять только сообщения адресованные лично мне. И добавили звук по приходу сообщения для меня.
Теперь приватные сообщения. Хорошо бы выделить их из общего потока, чтобы бросались в глаза и нужна возможность, ответить отправителю приватно одним щелчком мыши. Предлагаю выделить красным цветом фразу " private [имя] " и сделать из нее ссылку, причем ссылка должна быть хитрая, туда подставляется:
- если отправитель не я - имя отправителя
- если отправитель я - имя того к кому я обращался.
Второй вариант поясню. Допустим, написал я в чате:
01:01 [мое имя] private [Вася Пупкин] Привет! И хочу добавить фразу "Как дела?", удобнее будет щелкнуть по " private [Вася Пупкин]" чтобы в строке ввода появилось это именно "private [Вася Пупкин]", а не "private [мое имя] " ведь отправитель сообщения я. Вот такая функция получилась:
// выделить строку?
var from_me = (FromName == _global.mylogin); // эту строчку писал я
var to_me = ( txt.indexOf(" to ["+_global.mylogin+"] ")>0 ); //ко мне обращаются

if ((j=txt.indexOf(" private ["))>0)    // это я пишу кому-то приват или ко мне приватно
{
    var j2 = txt.indexOf("] ",j);
    if (j2>0) { // если не удалось найти "]" - текст не меняем
        var ToName = txt.substring(j+10, j2);    // кому приват
        if (ToName == _global.mylogin) { ToName = FromName; to_me = true; }    // выделяем строку
        // делаем ссылку в тексте
        txt = txt.substr(0,j)+" <a href=\"asfunction:AddPName,"+
            ToName+"\"><font color=\"#"+chatColors.prv+"\"><u>"+
            txt.substring(j+1, j2+1) + "</u></font></a>" +
            txt.substr(j2+1);
    }
}
var sel = to_me or from_me;
if (Filter == 3 and !sel) return ; // фильтрация
if (to_me) _root.mesSound.start(); // звук по приходу сообщения для меня
Ух... давайте теперь сделаем выделение ссылок на Интернет странички, т.е. все что начинается с www. и http:// Здесь надо учесть, что имя юзера может быть похоже на ссылку, например, кто-то назовет себя www.ru , во избежание путаницы надо отличать имя от обычного текста. Т.к. в чате имя всегда выделяется квадратными скобками с обоих сторон, то при нахождении в тексте ссылки достаточно будет проверить не содержится ли после нее закрывающейся квадратной скобки без открытой. Например, строку: " www.ru что-то еще ]" считаем именем, а не ссылкой. Перед ссылкой обязательно должен быть пробел, после ссылки или конец строки, или пробел, или точка, или запятая. Оформим поиск и выделение ссылок в тексте отдельной функции:
// Ищет в тексте ссылки (URL) и выделяет их
function SelectURLs(txt:String) {
var z=[" www."," http://"," https://"," ftp://"];
for (var i1 in z) {
    var i, j,j2,j3, last_i=0;
    while ((i=txt.indexOf(z[i1], last_i)) > 0) {
        j = txt.indexOf(" ", i+1);    // ищем конец ссылки
        if (j<0) j=txt.length;
        j--;
        // точки и запятые в конце пропускаем
        while (txt.charAt(j)=="." Or txt.charAt(j)==",") { j-- }
        j2=txt.indexOf("]",i);
        if (j2>0) {    // после ссылки есть закрывающаяся квадратная скобка
            j3=txt.indexOf("[",i);
            if (j3>0) { // там есть и открывающаяся
                if (j3>j2) { // но она расположена дальше по тексту
                    // эту ссылку не обрабатываем, переходим дальше
                    last_i = j; continue;
                }
            } else { // открывающейся нету, значит эту ссылку не обрабатываем, переходим дальше
                last_i = j; continue;
            }
        }
        // выделяем ссылку
        var url = txt.substring(i+1, j+1);
        // добавим вначале http:// если не указано
        if (url.indexOf("://")<0) { url = "http://"+url; }
        txt = txt.substr(0, i+1) +
            " <a href=\""+url+"\" target=\"_blank\">"+
            "<font color=\"#"+chatColors.url+"\"><u>"+
            url+"</u></font></a>"+txt.substr(j+1);
        // продолжаем поиск ссылок, после этой
        last_i = i + 34 + url.length*2;
    }
}
return txt;
}
Вот такая функция... Пояснять не буду, есть комментарии, простая работа со строчками. То, что на Perl записывается одной строкой, тут выросло вот в такую жуткую функцию... и почему во Flash нет regex??? Ладно, отвлеклись от темы.
Я добавил еще один цвет, для выделения ссылок:
var chatColors = {            // цвета в чате
        date:"00A000",     // дата сообщения
        system:"A00000",    // системные сообщения
        name:"0000CC",    // подсветка имени
        prv:"F00000",        // приватные сообщения
        url:"0000FE"        // подсветка ссылок
    };
Функцию SelectURLs надо вызывать в самом начале AppendText, до того, как будут обработаны "to", "private" и прочие навороты, иначе при наличии сложного форматирования в строке функция поиска ссылок может глючить.

Сохраним текущие наработки: chat v4.zip

8. Смайлики.

Итак, осталась самая "малость", показать графические смайлики в тексте. В 7-ой Flash ввели поддержку тега <img>, но! Картинка не показывается в строке текста, она может быть отображена слева или справа от текста и текст будет ее "обтекать", что нам совсем не подходит... Если бы был нормально реализован тег <img> показ смайликов занял бы 10-15 строк кода, а так, придется здорово попотеть...
На мой взгляд, есть два приемлемых решения:
1. Отказаться от показа произвольного количества смайликов прямо в тексте, сделать отображение одного смайла рядом с ником. Например, так:



Т.е. возможность выбрать только один смайлик, который и будет отображаться рядом с именем в тексте, дату перенести вправо, чтобы не мешалась (смайлик показывать через тег <img>).
Что сказать... решение вполне хорошее, но не хочется идти на компромисс в этом вопросе... ведь было заявлено, создание полноценного чата!
2. Заменить в тексте смайлики на количество пробелов равное ширине смайлика, а смайлики выводить поверх текста, программно сдвигая их вверх/вниз при скроллинге текста. Задача очень сложная, главная проблема - это вычислить икс координату, куда выводить смайлик. Если имеем дело с короткой строкой текста, то можно создать невидимый TextField, записать в него нашу строку до смайлика, и TextField.textWidth покажет нам ширину этого текста.


простая строка

Но, если строка длинная, т.е. отображается как две, тут выяснить ширину нижней части текста почти невозможно.


длинная строка
"Почти" - потому что, ничего невозможного нет ;) Можно сделать это в два этапа, вычислить длину текста без переноса на новую строку (сделать очень длинный TextField), а потом считаем ширину с переносом, и из первой ширины вычитаем вторую, получается то, что осталось на второй строке. Но, нижняя половина строки может быть длиннее верхней! Это раз, а второе - строка может быть разбита и на три части. Так что надо идти другим путем.
Для того чтобы выяснить длину нижней половины строки, придется программно выяснить, в каком месте произошел перевод строки... Создаем невидимый TextField и в цикле добавляем по одному слову из текста, до тех пор, пока высота TextField равна высоте одной строки, как только произошел переход строки (высота увеличилась), делаем шаг назад, на предыдущее слово, значит оно последнее, следующее слово будет отображаться на второй строке. Обрезаем текст до этого слова и начинаем все сначала. Фактически программно обнаруживаем, где происходит переход на другую строку, чтобы выяснить длину оставшейся части текста. И опять не все здесь так просто: придется разобраться, по каким символам Flash режет текст, для переноса на следующую строчку? Это пробел, тире, запятая, точка и другая пунктуация, кроме того, и слово вида "слово123" он тоже разделит на две части: "слово" оставит на первой строке, а "123" перенесет на следующую (выяснил экспериментально). В общем, все очень сложно.
Можно не разбираться в том, как Flash переносит строчки, а делать это самостоятельно, по своим правилам (например, только по пробелу, но учтите, что у нас строка с HTML форматированием, т.е. надо будет пропускать теги <...> и символы типа &quot; считать за один) и в конце первой строки ставим принудительно тег <BR>. Теперь Flash сделает перевод на вторую строчку именно там, где мы ему это указали. На мой взгляд, это единственное приемлемое решение.
Закрываем проект с чатом, открываем новый, где будем создавать свой TextField, заточенный именно под наш чат и правильное отображение текста с графическими смайликами.
Создаем новый документ, добавляем два TextField один побольше (name: chat, Dynamic text, Multiline) - это основное окно чата; второй в одну строку (name: input_txt, Input Text, Single line) - это поле для ввода текста.
Кнопочки не нужны, сделаем, чтобы по нажатию клавиши Enter происходило добавление текста:
myListener = new Object();
myListener.onKeyDown = function() {
    if (Key.getCode() == Key.ENTER) {
        addHTMLstring(input_txt.text);
        input_txt.text = "";       
    }
}
Key.addListener(myListener);
Шрифт и его размер задаются в параметрах основного TextField (chat) и считаем их неизменными. Итак, объявляем константы, глобальные переменные и функцию инициализации:
var SmileName : Array = new Array    // имена смайликов. smile0 == :) , smile1 == :rotate: и т.д.
    (":)", ":rotate:", ":yes:", ":no:", ":P");
var BaseSmileDepth : Number = 1200;    // базовая глубина (depth) смайлов   
var SmileWidth : Array = new Array();    // Ширина каждого смайлика
var SmileOnLine : Array =new Array();    // перечень параметров всех смайлов, по строчкам
var SmileVisibled : Array = new Array(); // перечень видимых смайлов
var txtField : TextField; // ссылка на TextField
var txtFieldWidth : Number; // ширина текстового поля
var textFieldFormat : TextFormat; // форматирование текста
var BaseLineHeight : Number; // смещение базовой линии текста

var LineCounter : Number = 1;    // счетчик строк
var ScaleSmileKf : Number = 1;    // коэф. сжатия смайлов под размер шрифта

// Инициализация параметров
function InitChatTextField(TextFieldLink : TextField):Void {
    txtField = TextFieldLink;    // запоминаем ссылку на TextField
    with (txtField) {    // устанавливаем необходимые параметры
        html = multiline = true;
        htmlText = "Chat v0.1 (by <a href=\"mailto:merlin@delphimaster.ru\"><u>Aleksey Merlin</u></a>)";
    }
    LineCounter = 2;    // счетчик строк в чате

    var NormalSmileHeight : Number;    // высота обыкновенного смайла
    // Нужно вычислить ширину всех смайлов
    for (var i in SmileName) {
        _root.attachMovie("smile"+i, "tmp_test_smile", BaseSmileDepth);
        SmileWidth[i] = _root.tmp_test_smile._width;
        if (i==0) NormalSmileHeight = _root.tmp_test_smile._height;
        _root.tmp_test_smile.removeMovieClip();
    }

    // Запомним высоту строки и смещение базовой линии
    // подробнее см. картинку в хелпе про getTextExtent
    textFieldFormat = txtField.getTextFormat();
    var metrics : Object = textFieldFormat.getTextExtent("Wq");
    OneLineHeight = metrics.height + 1;
    BaseLineHeight = metrics.ascent + 2;
    ScaleSmileKf = metrics.ascent / NormalSmileHeight;
    txtFieldWidth = txtField._width - 4;
    if (tf.leftMargin > 0) txtFieldWidth - tf.leftMargin;
    if (tf.rightMargin > 0) txtFieldWidth - tf.rightMargin;
   
    // При скроллинге, нам надо смещать смайлики.
    // Добавляем свой Listener на скроллинг
    var myScrollListener = new Object();
    myScrollListener.onScroller = function() { UpdateSmiles() }
    txtField.addListener(myScrollListener);
    // обнуление массивов
    SmileOnLine = []; SmileVisibled = [];
}
Функции инициализации передаем ссылку на TextField. Она запоминает ширину всех смайликов (чтобы не считать каждый раз при добавлении нового) и параметры срок: высота одной строки, коэффициент сжатия смайликов в зависимости от размера шрифта (не будем же мы выводить смайлики одинакового размера при шрифте 10 и 20 ?), максимальную допустимую ширину текста и смещение базовой линии текста. О последнем параметре подробнее. Для точного позиционирования смайликов по вертикали, нам необходимо знать высоту строки (т.к. шрифт и размер считаем константами, то она не должна меняться) и смещение до базовой линии (ascent - подробнее см. Help про getTextExtent для Flash MX 2004). Если выводить смайлик без учета базовой линии, фактически от верхнего края строки, то он будет находиться немного выше букв (или ниже зависит от размера смайлика), поэтому необходимо привязать точку отсчета к базовой линии, на которой "лежат" буквы. Для этого мы запоминаем ascent, а в библиотеке смайликов выравниваем их от точки отсчета вверх и вправо, вот так:

Теперь о массивах для смайликов. В текстовой строке подменяем смайлик на пробелы, выводим эти пробелы, вычисляем икс позицию графического смайлика и добавляем эти параметры в глобальный массив. Информацию обо всех смайликах в тексте будем хранить в двухмерном массиве, первое измерение - номер строки в тексте (для удобства нахождения нужных смайликов при скроллинге), второе измерение - непосредственно перечисление параметров всех смайликов на этой строке.
Т.е. примерно так:
строка 1: пусто
строка 2: { link:"smile7", x:20 }, { link: "smile0", x:90 }
строка 3: { link: "smile4", x:54 }
строка 4: пусто
. . .
Храним только параметры смайликов (имя в библиотеке и икс координату), можно было бы создать сразу все смайлики и только включать/выключать их видимость (visible) по мере необходимости. Но я боюсь, что когда смайликов будет порядка нескольких тысяч, все это начнет жутко тормозить, да и расходовать оперативную память так небрежно нельзя. Поэтому, мы будем удалять Movie Clip со смайликами, которые ушли из зоны видимости, и создавать для тех, кто только что появился.
При скроллинге текста, нужно будет выяснить, какие смайлики удалить, а какие создать. Для этого создаем массив, в котором будут перечислены все смайлики, которые созданы и видны на текущий момент. Номер позиции в этом массиве определяет имя Movie Clip со смайлом и глубину (depth). При скроллинге мы получаем номер верхней и нижней строки текста в текстовом поле. Пробегаемся по массиву с видимыми смайликами и удаляем те смайлики, которые находятся на строчках выше или ниже видимых. Заодно помечаем, смайлики каких строчек уже отображены. После этого создаем только те смайлики, которые находятся на строчках, которых нет в массиве видимых смайлов.
Вот так выглядит функция, вызываемая при добавлении новых смайлов и при скроллинге, которая выполняет все вышеописанное.
// Обновляет смайлики при необходимости
function UpdateSmiles():Void {
    // какие строчки текста сейчас видны? (min/max)
    var min : Number = txtField.scroll;
    var max : Number = txtField.bottomScroll;
    // массив для проверки, смайлики каких строчек отображены, а какие надо создать
    var CheckLines : Array = new Array(max-min+1);
    var i,j : Number;
    // Проверяем, какие смайлики нужно удалить
    for (i = 0; i < SmileVisibled.length; i++) {
        if (SmileVisibled[i] != undefined) {
            if (SmileVisibled[i].LineNum < min or SmileVisibled[i].LineNum > max) { // удаляем
                SmileVisibled[i].removeMovieClip();
                delete SmileVisibled[i];
            } else {    // помечаем, какие строчки отображены
                CheckLines[SmileVisibled[i].LineNum - min] = true;
                // корректируем _y координату смайла
                SmileVisibled[i]._y = txtField._y + BaseLineHeight + (SmileVisibled[i].LineNum - min)*OneLineHeight;
            }
        }
    }
    // Проверяем, есть ли строчки, которые в зоне видимости, но еще не отображены
    for (i=min; i<=max; i++) {
        if (SmileOnLine[i] != undefined and !CheckLines[i-min]) {    // вот она, надо создавать смайлы
            for(j=0; j<SmileOnLine[i].length; j++) {    // проходим по всем смайлам этой строки
                // ищем свободное место в массиве SmileVisibled
                var empty : Number = 0;
                while (SmileVisibled[empty] != undefined) empty++;
                // создаем смайл
                txtField._parent.attachMovie(SmileOnLine[i][j].link, "smiles_tmp_"+empty, BaseSmileDepth+empty+1);
                SmileVisibled[empty] = txtField._parent["smiles_tmp_"+empty];
                SmileVisibled[empty].LineNum = i;    // запоминаем номер строки, на которой стоит смайлик
                SmileVisibled[empty]._x = txtField._x + SmileOnLine[i][j].x + 2;
                SmileVisibled[empty]._y = txtField._y + BaseLineHeight + (i - min)*OneLineHeight;
                SmileVisibled[empty]._xscale = SmileVisibled[empty]._yscale = ScaleSmileKf*100;
            }
        }
    }
}
На каждой строке комментарий, мне добавить нечего. Остается написать парсер строки. Алгоритм работы описан выше, там 90% - это работа со строчками. Главное считать ширину строки, как только она превышает границы TextField вставляем тег <BR>. Единственное интересное место - преобразование смайликов.
for (i in SmileName) {    // проверяем, далее идет смайлик?
    if (ParseParam.s.substr(ParseParam.pos, SmileName[i].length) == SmileName[i]) {
        // да, нашли смайлик.
        w = textFieldFormat.getTextExtent(" ").width;    // вычисляем ширину пробела
        // сколько нужно нарисовать пробелов по ширине смайлика?
        c = Math.floor(SmileWidth[i]*ScaleSmileKf / w)+1;
        // Смайл влезет на текущую строку?
        if (ParseParam.width + w*c >= txtFieldWidth) {    // нет, не помещается.
            // Делаем переход на новую строку
            EnterBR();
        }
        ParseParam.width += w*c;    // увеличиваем счетчик ширины строки
        var empty : String = " ";    // делаем пробелы
        while (--c > 0) empty+=" ";
        // заменяем смайл на пробелы
        ParseParam.s = ParseParam.s.substring(0, ParseParam.pos) + empty +
                        ParseParam.s.substr(ParseParam.pos+SmileName[i].length);
        ParseParam.pos += empty.length;    // переходим к следующему символу
        // запоминаем позицию нового смайла
        if (SmileOnLine[LineCounter] == undefined) SmileOnLine[LineCounter] = new Array();
        SmileOnLine[LineCounter].push(
            {    link : "smile"+i,
                x : Math.max(ParseParam.width - SmileWidth[i]*ScaleSmileKf, 2)
            } );
        break;
    }
}
На каждой строке комментарий. Полностью функцию парсера см. в исходнике (ссылка ниже).
Вот и все, остается только протестировать. Вот так смотрится наше творение:

Cмайлики добавляются, скролятся, динамично создаются и удаляются по мере необходимости.
То, что получилось, можно скачать здесь: chatTextField v1.zip
Скроллбара нет, так что прокручивать текст только колесиком мышки.
Прямо здесь можно и потестировать:

Важный момент. На то чтобы понять, почему все работает не так, у меня ушло трое суток, а причина, как всегда, банальна. У меня постоянно смайлики сдвигались по вертикали или горизонтали относительно текста, чем дальше по тексту расположен смайлик - тем больше сдвиг, такое впечатление, что неверно рассчитывалась ширина и высота букв... но как оказалось, все считалось правильно, проблема в другом. Как я обычно добавляю TextField? Ставлю курсор мыши в левый верхний угол, клик, грубо провожу до нижнего правого и далее уже в Properies правлю числа X,Y и ширина,высота, чтобы точно отпозиционировать поле. Так вот, при изменении ширины и высоты TextField не меняется его реальная высота и ширина, как я ожидал, а меняется масштаб по ширине и высоте!!! (для тех, кто начинал изучение Flash с рисования мультиков и баннеров, это наверное очевидно, для меня же было открытием). И если у TextField масштаб не 100% по вертикали или горизонтали, то смайлики будут позиционировать неверно. Надо или ввести учет масштаба TextField или требовать, чтобы оно было только 100%. Второе гораздо проще :)

Теперь переносим все написанное в чат и, собственно все. Осталось навести глянец, и протестировать хорошенько. В конечном варианте, я убрал стандартные компоненты флэша TextInput, Button и List, т.к. с их использованием размер флешки чата получается 64кб., а без них 16кб. разница существенная.
Конечный вариант чата, скачать можно здесь: chat_final.zip

На этом и закончу. Совершенствовать созданное можно бесконечно, но тогда эта статья никогда не будет написана до конца :)
С уважением, Алексей (Merlin).

   Внимание! Запрещается перепечатка данной статьи или ее части без согласования с автором. Если вы хотите разместить эту статью на своем сайте или издать в печатном виде, свяжитесь с автором.
Автор статьи: Алексей (Merlin)
  

Другие статьи Наверх


  Рейтинг@Mail.ru     Титульная страница Поиск, карта сайта Написать письмо