Спасибо.
Почти получилось написать свой скроллбар: устанавливаю text_field.scrollV в значение, которое соответствует текущему положения ползунка.
Правда в этом случае пришлось отказаться от скроллинга колесиком мыши. Либо колесиком текстовое поле само себя скролит (и скроллбар в соответствии двигает ползунок), либо если используется полоса скроллбар, то на колесико не реагируем.
Во что вышло (пока без кнопок, ползунок - тоже самостоятельно написанная кнопка)

Код AS3:
public class ScrollBar extends MovieClip
{
private var background :MovieClip;
private var slider :BasicButton;
private var max_value :Number;
private var delta_y :Number = 10;
private var anchor_y :Number;
private var vis_lines_count :int;
private var text_field :TextField;
private var enable_whell :Boolean;
public function ScrollBar()
{
super();
background = this.getChildByName("bg") as MovieClip;
max_value = background.height;
slider = new BasicButton(this.getChildByName("slider_mc") as MovieClip);
slider.addEventListener(MouseEvent.MOUSE_DOWN, sliderMouseDownHandler);
slider.addEventListener(MouseEvent.MOUSE_UP, sliderMouseUpHandler);
this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
enable_whell = true;
}
public function attachTextField(textField :TextField):void{
text_field = textField;
text_field.addEventListener(Event.SCROLL, scrollTextHandler);
vis_lines_count = Math.floor(textField.height * textField.numLines / textField.textHeight);
scrollTextHandler(null);
}
private function sliderMouseDownHandler(event:MouseEvent):void{
startSlide();
}
private function sliderMouseUpHandler(event:MouseEvent):void{
stopSlide();
}
private function mouseOutHandler(event:MouseEvent):void{
stopSlide();
}
private function sliderMouseMoveHandler(event:MouseEvent):void{
slider.Clip.y = this.mouseY - anchor_y;
normalizePosition();
}
private function enterFrameHandler(event :Event):void{
if (text_field && this.visible && !enable_whell){
var n:int = text_field.numLines;
var percent :Number = slider.Clip.y/(max_value - slider.Clip.height)
text_field.scrollV = Math.round(percent*(n-(vis_lines_count-1))) + 1;
}
}
private function scrollTextHandler(event :Event):void{
if (text_field && enable_whell){
var n:int = text_field.numLines;
var cur:int = text_field.bottomScrollV;
var percent :Number = (cur - vis_lines_count)/(n - vis_lines_count)
slider.Clip.y = (max_value - slider.Clip.height)*percent;
visible = (n >= vis_lines_count);
}
}
private function normalizePosition():void{
if (slider.Clip.y > max_value - slider.Clip.height)
slider.Clip.y = max_value - slider.Clip.height;
if (slider.Clip.y < 0)
slider.Clip.y = 0;
}
private function startSlide():void{
if (!this.hasEventListener(MouseEvent.MOUSE_OUT))
this.addEventListener(MouseEvent.MOUSE_OUT, mouseOutHandler);
slider.Clip.startDrag(false, new Rectangle(0, 0, 0, max_value - slider.Clip.height));
enable_whell = false;
}
private function stopSlide():void{
if (this.hasEventListener(MouseEvent.MOUSE_OUT))
this.removeEventListener(MouseEvent.MOUSE_OUT, mouseOutHandler);
slider.Clip.stopDrag();
enable_whell = true;
}
private function slideDown():void{
slider.Clip.y += delta_y;
normalizePosition();
}
private function slideUp():void{
slider.Clip.y -= delta_y;
normalizePosition();
}
public function destoy():void{
slider.destroy();
try{slider.removeEventListener(MouseEvent.MOUSE_DOWN, sliderMouseDownHandler);}
catch(e:Error){};
try{slider.removeEventListener(MouseEvent.MOUSE_UP, sliderMouseUpHandler);}
catch(e:Error){};
try{this.removeEventListener(MouseEvent.MOUSE_OUT, mouseOutHandler);}
catch(e:Error){};
try{this.removeEventListener(Event.ENTER_FRAME, enterFrameHandler);}
catch(e:Error){};
if (text_field){
try{text_field.removeEventListener(Event.SCROLL, scrollTextHandler);}
catch(e:Error){};
}
}
}